diff --git a/cabal-install-parsers/Changelog.md b/cabal-install-parsers/Changelog.md index 93fca810..e1696505 100644 --- a/cabal-install-parsers/Changelog.md +++ b/cabal-install-parsers/Changelog.md @@ -1,3 +1,7 @@ +## 0.6.4 + +- Add support for reading project files with conditionals. + ## 0.6.3 - Drop support for GHC prior 8.8.4 diff --git a/cabal-install-parsers/cabal-install-parsers.cabal b/cabal-install-parsers/cabal-install-parsers.cabal index 3f51819a..54466076 100644 --- a/cabal-install-parsers/cabal-install-parsers.cabal +++ b/cabal-install-parsers/cabal-install-parsers.cabal @@ -1,6 +1,6 @@ cabal-version: 2.2 name: cabal-install-parsers -version: 0.6.3 +version: 0.6.4 synopsis: Utilities to work with cabal-install files description: @cabal-install-parsers@ provides parsers for @cabal-install@ files: diff --git a/cabal-install-parsers/src/Cabal/Index.hs b/cabal-install-parsers/src/Cabal/Index.hs index fbf761a8..03661b83 100644 --- a/cabal-install-parsers/src/Cabal/Index.hs +++ b/cabal-install-parsers/src/Cabal/Index.hs @@ -399,7 +399,7 @@ indexMetadata indexFilepath mindexState = do f :: Maybe TmpPackageInfo -> Maybe TmpPackageInfo f Nothing = Just TmpPackageInfo - { tmpPiVersions = Map.singleton ver TmpReleaseInfo + { tmpPiVersions = Map.singleton ver TmpReleaseInfo { tmpRiRevision = 0 , tmpRiTarOffset = offset , tmpRiCabalHash = Just digest diff --git a/cabal-install-parsers/src/Cabal/Project.hs b/cabal-install-parsers/src/Cabal/Project.hs index d8e907f8..0815eae1 100644 --- a/cabal-install-parsers/src/Cabal/Project.hs +++ b/cabal-install-parsers/src/Cabal/Project.hs @@ -1,10 +1,11 @@ -{-# LANGUAGE CPP #-} -{-# LANGUAGE DeriveFoldable #-} -{-# LANGUAGE DeriveFunctor #-} -{-# LANGUAGE DeriveGeneric #-} -{-# LANGUAGE DeriveTraversable #-} -{-# LANGUAGE MultiWayIf #-} -{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE CPP #-} +{-# LANGUAGE DeriveFoldable #-} +{-# LANGUAGE DeriveFunctor #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE DeriveTraversable #-} +{-# LANGUAGE MultiWayIf #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} -- | License: GPL-3.0-or-later AND BSD-3-Clause -- module Cabal.Project ( @@ -15,6 +16,8 @@ module Cabal.Project ( -- * Parse project readProject, parseProject, + readProjectWithConditionals, + parseProjectWithConditionals, -- * Resolve project resolveProject, ResolveError (..), @@ -25,6 +28,7 @@ module Cabal.Project ( import Control.DeepSeq (NFData (..)) import Control.Exception (Exception (..), throwIO) +import Control.Monad (unless) import Control.Monad.IO.Class (liftIO) import Control.Monad.Trans.Except (ExceptT, runExceptT, throwE) import Data.Bifoldable (Bifoldable (..)) @@ -55,6 +59,7 @@ import qualified Data.Map.Strict as M import qualified Distribution.CabalSpecVersion as C import qualified Distribution.FieldGrammar as C import qualified Distribution.Fields as C +import qualified Distribution.Fields.ConfVar as C import qualified Distribution.PackageDescription as C import qualified Distribution.Parsec as C @@ -71,6 +76,12 @@ infixl 1 <&> -- $setup -- >>> :set -XOverloadedStrings +-- >>> import Data.String (fromString) +-- >>> import qualified Distribution.PackageDescription as C +-- >>> import Text.Show (showListWith) +-- >>> import Data.Functor.Classes (liftShowsPrec) +-- >>> let sB (C.CondBranch c t f) = showString "CondBranch _ " . showParen True (sT t) . showChar ' ' . liftShowsPrec (\_ -> sT) undefined 11 f; sT (C.CondNode x c xs) = showString "CondTree " . showsPrec 11 x . showString " _ " . showListWith sB xs +-- >>> pp x = putStrLn (either show (flip sT "") x) -- | @cabal.project@ file data Project uri opt pkg = Project @@ -152,7 +163,7 @@ instance (NFData c, NFData b, NFData a) => NFData (Project c b a) where rnf x7 `seq` rnf x8 `seq` rnf x9 `seq` rnfList rnfPrettyField x10 where - rnfList :: (a -> ()) -> [a] -> () + rnfList :: (x -> ()) -> [x] -> () rnfList _ [] = () rnfList f (x:xs) = f x `seq` rnfList f xs @@ -179,6 +190,13 @@ readProject fp = do prj1 <- resolveProject fp prj0 >>= either throwIO return readPackagesOfProject prj1 >>= either throwIO return +readProjectWithConditionals :: FilePath -> IO (C.CondTree C.ConfVar () (Project URI Void (FilePath, C.GenericPackageDescription))) +readProjectWithConditionals fp = do + contents <- BS.readFile fp + prj0 <- either throwIO return (parseProjectWithConditionals fp contents) + prj1 <- traverse (\p -> resolveProject fp p >>= either throwIO return) prj0 + traverse (\p -> readPackagesOfProject p >>= either throwIO return) prj1 + -- | Parse project file. Extracts only few fields. -- -- >>> fmap prjPackages $ parseProject "cabal.project" "packages: foo bar/*.cabal" @@ -207,6 +225,46 @@ parseProject = parseWith $ \fields0 -> do parseSec _ = return id +-- | Parse project files with conditionals. +-- +-- >>> pp $ fmap (fmap prjPackages) $ parseProjectWithConditionals "cabal.project" "packages: foo bar/*.cabal" +-- CondTree ["foo","bar/*.cabal"] _ [] +-- +-- >>> pp $ fmap (fmap prjPackages) $ parseProjectWithConditionals "cabal.project" $ fromString $ unlines [ "packages: foo bar/*.cabal", "if impl(ghc >=9)", " packages: quu" ] +-- CondTree ["foo","bar/*.cabal"] _ [CondBranch _ (CondTree ["quu"] _ []) Nothing] +-- +-- >>> pp $ fmap (fmap prjPackages) $ parseProjectWithConditionals "cabal.project" $ fromString $ unlines [ "packages: foo bar/*.cabal", "if impl(ghc >=9)", " packages: quu", "if impl(ghc >=10)", " packages: zoo" ] +-- CondTree ["foo","bar/*.cabal"] _ [CondBranch _ (CondTree ["quu"] _ []) Nothing,CondBranch _ (CondTree ["zoo"] _ []) Nothing] +-- +-- >>> pp $ fmap (fmap prjPackages) $ parseProjectWithConditionals "cabal.project" $ fromString $ unlines [ "packages: foo bar/*.cabal", "if impl(ghc >=9)", " packages: quu", "else", " packages: zoo" ] +-- CondTree ["foo","bar/*.cabal"] _ [CondBranch _ (CondTree ["quu"] _ []) (Just CondTree ["zoo"] _ [])] +-- +-- >>> pp $ fmap (fmap prjPackages) $ parseProjectWithConditionals "cabal.project" $ fromString $ unlines [ "packages: foo bar/*.cabal", "if impl(ghc >=9)", " packages: quu", "elif impl(ghc >=10)", " packages: zoo", "else", " packages: yyz" ] +-- CondTree ["foo","bar/*.cabal"] _ [CondBranch _ (CondTree ["quu"] _ []) (Just CondTree [] _ [CondBranch _ (CondTree ["zoo"] _ []) (Just CondTree ["yyz"] _ [])])] +-- +parseProjectWithConditionals :: FilePath -> ByteString -> Either (ParseError NonEmpty) (C.CondTree C.ConfVar () (Project Void String String)) +parseProjectWithConditionals = parseWith $ \fields0 -> flip parseCondTree fields0 $ \fields1 sections -> do + let fields2 = M.filterWithKey (\k _ -> k `elem` knownFields) fields1 + parse fields0 fields2 sections + where + knownFields = C.fieldGrammarKnownFieldList $ grammar [] + + parse :: [C.Field a] -> C.Fields C.Position -> [[C.Section C.Position]] -> C.ParseResult (Project Void String String) + parse otherFields fields sections = do + let prettyOtherFields = map void $ C.fromParsecFields $ filter otherFieldName otherFields + prj <- C.parseFieldGrammar C.cabalSpecLatest fields $ grammar prettyOtherFields + foldl' (&) prj <$> traverse parseSec (concat sections) + + -- Special case for source-repository-package. If you add another such + -- special case, make sure to update otherFieldName appropriately. + parseSec :: C.Section C.Position -> C.ParseResult (Project Void String String -> Project Void String String) + parseSec (C.MkSection (C.Name _pos name) [] fields) | name == sourceRepoSectionName = do + let fields' = fst $ C.partitionFields fields + repos <- C.parseFieldGrammar C.cabalSpecLatest fields' sourceRepositoryPackageGrammar + return $ over prjSourceReposL (++ toList (srpFanOut repos)) + + parseSec _ = return id + -- | Returns 'True' if a field should be a part of 'prjOtherFields'. This -- excludes any field that is a part of 'grammar' as well as -- @source-repository-package@ (see 'parseProject', which has a special case @@ -377,3 +435,61 @@ readPackagesOfProject :: Project uri opt FilePath -> IO (Either (ParseError NonE readPackagesOfProject prj = runExceptT $ for prj $ \fp -> do contents <- liftIO $ BS.readFile fp either throwE (\gpd -> return (fp, gpd)) (parsePackage fp contents) + +------------------------------------------------------------------------------- +-- Read package files +------------------------------------------------------------------------------- + +parseCondTree + :: forall a. (C.Fields C.Position -> [[C.Section C.Position]] -> C.ParseResult a) -- ^ parse + -> [C.Field C.Position] + -> C.ParseResult (C.CondTree C.ConfVar () a) +parseCondTree subparse = go + where + go fields = do + let (fs, ss) = C.partitionFields fields + (ss', branches) <- second concat . unzip <$> traverse (goIfs id id) ss + x <- subparse fs ss' + return $ C.CondNode x () branches + + goIfs + :: ([C.Section C.Position] -> [C.Section C.Position]) + -> ([C.CondBranch C.ConfVar () a] -> [C.CondBranch C.ConfVar () a]) + -> [C.Section C.Position] + -> C.ParseResult ([C.Section C.Position], [C.CondBranch C.ConfVar () a]) + goIfs accS accB [] = do + return (accS [], accB []) + goIfs accS accB (C.MkSection (C.Name pos name) args fields : sections) + | name == "if" = do + test' <- C.parseConditionConfVar args + fields' <- go fields + goElse (C.CondBranch test' fields') accS accB sections + | name == "else" = do + C.parseFailure pos "standalone else" + return ([], []) + | name == "elif" = do + C.parseFailure pos "standalone elif" + goIfs accS accB sections + goIfs accS accB (section : sections) = do + goIfs (accS . (section :)) accB sections + + goElse + :: (Maybe (C.CondTree C.ConfVar () a) -> C.CondBranch C.ConfVar () a) + -> ([C.Section C.Position] -> [C.Section C.Position]) + -> ([C.CondBranch C.ConfVar () a] -> [C.CondBranch C.ConfVar () a]) + -> [C.Section C.Position] + -> C.ParseResult ([C.Section C.Position], [C.CondBranch C.ConfVar () a]) + goElse make accS accB (C.MkSection (C.Name pos name) args fields : sections) + | name == "else" = do + unless (null args) $ C.parseFailure pos "arguments passed to else" + fields' <- go fields + let condTree = make (Just fields') + goIfs accS (accB . (condTree :)) sections + | name == "elif" = do + test' <- C.parseConditionConfVar args + fields' <- go fields + emptyA <- subparse mempty [] + goElse (make . Just . C.CondNode emptyA () . pure . C.CondBranch test' fields') accS accB sections + goElse make accS accB sections = do + let condTree = make Nothing + goIfs accS (accB . (condTree :)) sections diff --git a/fixtures/conditionals.args b/fixtures/conditionals.args new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/conditionals.bash b/fixtures/conditionals.bash new file mode 100644 index 00000000..bef079e5 --- /dev/null +++ b/fixtures/conditionals.bash @@ -0,0 +1,527 @@ +# SUCCESS +# *INFO* Generating Bash script for testing for GHC versions: 8.0.1 8.0.2 8.2.1 8.2.2 8.4.1 8.4.2 8.4.3 8.4.4 8.6.1 8.6.2 8.6.3 8.6.4 8.6.5 8.8.1 8.8.2 8.8.3 8.8.4 8.10.1 8.10.2 8.10.3 8.10.4 8.10.5 8.10.6 8.10.7 +#!/bin/bash +# shellcheck disable=SC2086,SC2016,SC2046 +# REGENDATA ["bash","conditionals.project"] + +set -o pipefail + +# Mode +############################################################################## + +if [ "$1" = "indocker" ]; then + INDOCKER=true + shift +else + INDOCKER=false +fi + +# Run configuration +############################################################################## + +CFG_CABAL_STORE_CACHE="" +CFG_CABAL_REPO_CACHE="" +CFG_JOBS="8.10.7 8.10.6 8.10.5 8.10.4 8.10.3 8.10.2 8.10.1 8.8.4 8.8.3 8.8.2 8.8.1 8.6.5 8.6.4 8.6.3 8.6.2 8.6.1 8.4.4 8.4.3 8.4.2 8.4.1 8.2.2 8.2.1 8.0.2 8.0.1" +CFG_CABAL_UPDATE=false + +SCRIPT_NAME=$(basename "$0") +START_TIME="$(date +'%s')" + +XDG_CONFIG_HOME=${XDG_CONFIG_HOME:-$HOME/.config} + +# Job configuration +############################################################################## + +GHC_VERSION="non-existing" +CABAL_VERSION=3.2 +HEADHACKAGE=false + +# Locale +############################################################################## + +export LC_ALL=C.UTF-8 + +# Utilities +############################################################################## + +SGR_RED='\033[1;31m' +SGR_GREEN='\033[1;32m' +SGR_BLUE='\033[1;34m' +SGR_CYAN='\033[1;96m' +SGR_RESET='\033[0m' # No Color + +put_info() { + printf "$SGR_CYAN%s$SGR_RESET\n" "### $*" +} + +put_error() { + printf "$SGR_RED%s$SGR_RESET\n" "!!! $*" +} + +run_cmd() { + local PRETTYCMD="$*" + local PROMPT + if $INDOCKER; then + PROMPT="$(pwd) >>>" + else + PROMPT=">>>" + fi + + printf "$SGR_BLUE%s %s$SGR_RESET\n" "$PROMPT" "$PRETTYCMD" + + local start_time end_time cmd_duration total_duration + start_time=$(date +'%s') + + "$@" + local RET=$? + + end_time=$(date +'%s') + cmd_duration=$((end_time - start_time)) + total_duration=$((end_time - START_TIME)) + + cmd_min=$((cmd_duration / 60)) + cmd_sec=$((cmd_duration % 60)) + + total_min=$((total_duration / 60)) + total_sec=$((total_duration % 60)) + + if [ $RET -eq 0 ]; then + printf "$SGR_GREEN%s$SGR_RESET (%dm%02ds; %dm%02ds)\n" "<<< $PRETTYCMD" "$cmd_min" "$cmd_sec" "$total_min" "$total_sec" + else + printf "$SGR_RED%s$SGR_RESET\n" "!!! $PRETTYCMD" + exit 1 + fi +} + +run_cmd_if() { + local COND=$1 + shift + + if [ $COND -eq 1 ]; then + run_cmd "$@" + else + local PRETTYCMD="$*" + local PROMPT + PROMPT="$(pwd) (skipping) >>>" + + printf "$SGR_BLUE%s %s$SGR_RESET\n" "$PROMPT" "$PRETTYCMD" + fi +} + +run_cmd_unchecked() { + local PRETTYCMD="$*" + local PROMPT + if $INDOCKER; then + PROMPT="$(pwd) >>>" + else + PROMPT=">>>" + fi + + printf "$SGR_BLUE%s %s$SGR_RESET\n" "$PROMPT" "$PRETTYCMD" + + local start_time end_time cmd_duration total_duration cmd_min cmd_sec total_min total_sec + start_time=$(date +'%s') + + "$@" + + end_time=$(date +'%s') + cmd_duration=$((end_time - start_time)) + total_duration=$((end_time - START_TIME)) + + cmd_min=$((cmd_duration / 60)) + cmd_sec=$((cmd_duration % 60)) + + total_min=$((total_duration / 60)) + total_sec=$((total_duration % 60)) + + printf "$SGR_GREEN%s$SGR_RESET (%dm%02ds; %dm%02ds)\n" "<<< $PRETTYCMD" "$cmd_min" "$cmd_sec" "$total_min" "$total_sec" +} + +change_dir() { + local DIR=$1 + if [ -d "$DIR" ]; then + printf "$SGR_BLUE%s$SGR_RESET\n" "change directory to $DIR" + cd "$DIR" || exit 1 + else + printf "$SGR_RED%s$SGR_RESET\n" "!!! cd $DIR" + exit 1 + fi +} + +change_dir_if() { + local COND=$1 + local DIR=$2 + + if [ $COND -ne 0 ]; then + change_dir "$DIR" + fi +} + +echo_to() { + local DEST=$1 + local CONTENTS=$2 + + echo "$CONTENTS" >> "$DEST" +} + +echo_if_to() { + local COND=$1 + local DEST=$2 + local CONTENTS=$3 + + if [ $COND -ne 0 ]; then + echo_to "$DEST" "$CONTENTS" + fi +} + +install_cabalplan() { + put_info "installing cabal-plan" + + if [ ! -e $CABAL_REPOCACHE/downloads/cabal-plan ]; then + curl -L https://github.com/haskell-hvr/cabal-plan/releases/download/v0.6.2.0/cabal-plan-0.6.2.0-x86_64-linux.xz > /tmp/cabal-plan.xz || exit 1 + (cd /tmp && echo "de73600b1836d3f55e32d80385acc055fd97f60eaa0ab68a755302685f5d81bc cabal-plan.xz" | sha256sum -c -)|| exit 1 + mkdir -p $CABAL_REPOCACHE/downloads + xz -d < /tmp/cabal-plan.xz > $CABAL_REPOCACHE/downloads/cabal-plan || exit 1 + chmod a+x $CABAL_REPOCACHE/downloads/cabal-plan || exit 1 + fi + + mkdir -p $CABAL_DIR/bin || exit 1 + ln -s $CABAL_REPOCACHE/downloads/cabal-plan $CABAL_DIR/bin/cabal-plan || exit 1 +} + +# Help +############################################################################## + +show_usage() { +cat < $BUILDDIR/cabal/config <= 80200)) cabal.project "package servant" +echo_if_to $((HCNUMVER >= 80200)) cabal.project " ghc-options: -Werror=missing-methods" +echo_if_to $((HCNUMVER >= 80200)) cabal.project "package servant-client" +echo_if_to $((HCNUMVER >= 80200)) cabal.project " ghc-options: -Werror=missing-methods" +cat >> cabal.project <> cabal.project.local +run_cmd cat cabal.project +run_cmd cat cabal.project.local + +# dump install plan +put_info "dump install plan" +run_cmd $CABAL v2-build $ARG_COMPILER $ARG_TESTS $ARG_BENCH --dry-run all +run_cmd cabal-plan + +# install dependencies +put_info "install dependencies" +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 + +# build w/o tests +put_info "build w/o tests" +run_cmd $CABAL v2-build $ARG_COMPILER --disable-tests --disable-benchmarks all + +# build +put_info "build" +run_cmd $CABAL v2-build $ARG_COMPILER $ARG_TESTS $ARG_BENCH all + +# tests +put_info "tests" +run_cmd $CABAL v2-test $ARG_COMPILER $ARG_TESTS $ARG_BENCH all --test-show-details=direct + +# cabal check +put_info "cabal check" +change_dir "${PKGDIR_servant}" +run_cmd ${CABAL} -vnormal check +change_dir "${PKGDIR_servant_client}" +run_cmd ${CABAL} -vnormal check +change_dir "$BUILDDIR" + +# haddock +put_info "haddock" +run_cmd $CABAL v2-haddock --haddock-all $ARG_COMPILER --with-haddock $HADDOCK $ARG_TESTS $ARG_BENCH all + +# unconstrained build +put_info "unconstrained build" +run_cmd rm -f cabal.project.local +run_cmd $CABAL v2-build $ARG_COMPILER --disable-tests --disable-benchmarks all + + +# Done +run_cmd echo OK diff --git a/fixtures/conditionals.github b/fixtures/conditionals.github new file mode 100644 index 00000000..4725bef1 --- /dev/null +++ b/fixtures/conditionals.github @@ -0,0 +1,319 @@ +# SUCCESS +# *INFO* Generating GitHub config for testing for GHC versions: 8.0.1 8.0.2 8.2.1 8.2.2 8.4.1 8.4.2 8.4.3 8.4.4 8.6.1 8.6.2 8.6.3 8.6.4 8.6.5 8.8.1 8.8.2 8.8.3 8.8.4 8.10.1 8.10.2 8.10.3 8.10.4 8.10.5 8.10.6 8.10.7 +# This GitHub workflow config has been generated by a script via +# +# haskell-ci 'github' 'conditionals.project' +# +# To regenerate the script (for example after adjusting tested-with) run +# +# haskell-ci regenerate +# +# For more information, see https://github.com/haskell-CI/haskell-ci +# +# REGENDATA ["github","conditionals.project"] +# +name: Haskell-CI +on: + - push + - pull_request +jobs: + linux: + name: Haskell-CI - Linux - ${{ matrix.compiler }} + runs-on: ubuntu-24.04 + timeout-minutes: + 60 + container: + image: buildpack-deps:jammy + continue-on-error: ${{ matrix.allow-failure }} + strategy: + matrix: + include: + - compiler: ghc-8.10.7 + compilerKind: ghc + compilerVersion: 8.10.7 + setup-method: ghcup + allow-failure: false + - compiler: ghc-8.10.6 + compilerKind: ghc + compilerVersion: 8.10.6 + setup-method: ghcup + allow-failure: false + - compiler: ghc-8.10.5 + compilerKind: ghc + compilerVersion: 8.10.5 + setup-method: ghcup + allow-failure: false + - compiler: ghc-8.10.4 + compilerKind: ghc + compilerVersion: 8.10.4 + setup-method: ghcup + allow-failure: false + - compiler: ghc-8.10.3 + compilerKind: ghc + compilerVersion: 8.10.3 + setup-method: ghcup + allow-failure: false + - compiler: ghc-8.10.2 + compilerKind: ghc + compilerVersion: 8.10.2 + setup-method: ghcup + allow-failure: false + - compiler: ghc-8.10.1 + compilerKind: ghc + compilerVersion: 8.10.1 + setup-method: ghcup + allow-failure: false + - compiler: ghc-8.8.4 + compilerKind: ghc + compilerVersion: 8.8.4 + setup-method: ghcup + allow-failure: false + - compiler: ghc-8.8.3 + compilerKind: ghc + compilerVersion: 8.8.3 + setup-method: ghcup + allow-failure: false + - compiler: ghc-8.8.2 + compilerKind: ghc + compilerVersion: 8.8.2 + setup-method: ghcup + allow-failure: false + - compiler: ghc-8.8.1 + compilerKind: ghc + compilerVersion: 8.8.1 + setup-method: ghcup + allow-failure: false + - compiler: ghc-8.6.5 + compilerKind: ghc + compilerVersion: 8.6.5 + setup-method: ghcup + allow-failure: false + - compiler: ghc-8.6.4 + compilerKind: ghc + compilerVersion: 8.6.4 + setup-method: ghcup + allow-failure: false + - compiler: ghc-8.6.3 + compilerKind: ghc + compilerVersion: 8.6.3 + setup-method: ghcup + allow-failure: false + - compiler: ghc-8.6.2 + compilerKind: ghc + compilerVersion: 8.6.2 + setup-method: ghcup + allow-failure: false + - compiler: ghc-8.6.1 + compilerKind: ghc + compilerVersion: 8.6.1 + setup-method: ghcup + allow-failure: false + - compiler: ghc-8.4.4 + compilerKind: ghc + compilerVersion: 8.4.4 + setup-method: ghcup + allow-failure: false + - compiler: ghc-8.4.3 + compilerKind: ghc + compilerVersion: 8.4.3 + setup-method: ghcup + allow-failure: false + - compiler: ghc-8.4.2 + compilerKind: ghc + compilerVersion: 8.4.2 + setup-method: ghcup + allow-failure: false + - compiler: ghc-8.4.1 + compilerKind: ghc + compilerVersion: 8.4.1 + setup-method: ghcup + allow-failure: false + - compiler: ghc-8.2.2 + compilerKind: ghc + compilerVersion: 8.2.2 + setup-method: ghcup + allow-failure: false + - compiler: ghc-8.2.1 + compilerKind: ghc + compilerVersion: 8.2.1 + setup-method: ghcup + allow-failure: false + - compiler: ghc-8.0.2 + compilerKind: ghc + compilerVersion: 8.0.2 + setup-method: ghcup + allow-failure: false + - compiler: ghc-8.0.1 + compilerKind: ghc + compilerVersion: 8.0.1 + setup-method: ghcup + allow-failure: false + fail-fast: false + steps: + - name: apt-get install + run: | + apt-get update + apt-get install -y --no-install-recommends gnupg ca-certificates dirmngr curl git software-properties-common libtinfo5 libnuma-dev + - name: Install GHCup + run: | + mkdir -p "$HOME/.ghcup/bin" + curl -sL https://downloads.haskell.org/ghcup/0.1.50.1/x86_64-linux-ghcup-0.1.50.1 > "$HOME/.ghcup/bin/ghcup" + chmod a+x "$HOME/.ghcup/bin/ghcup" + - name: Install cabal-install + run: | + "$HOME/.ghcup/bin/ghcup" install cabal 3.14.2.0 || (cat "$HOME"/.ghcup/logs/*.* && false) + echo "CABAL=$HOME/.ghcup/bin/cabal-3.14.2.0 -vnormal+nowrap" >> "$GITHUB_ENV" + - name: Install GHC (GHCup) + if: matrix.setup-method == 'ghcup' + run: | + "$HOME/.ghcup/bin/ghcup" install ghc "$HCVER" || (cat "$HOME"/.ghcup/logs/*.* && false) + HC=$("$HOME/.ghcup/bin/ghcup" whereis ghc "$HCVER") + HCPKG=$(echo "$HC" | sed 's#ghc$#ghc-pkg#') + HADDOCK=$(echo "$HC" | sed 's#ghc$#haddock#') + echo "HC=$HC" >> "$GITHUB_ENV" + echo "HCPKG=$HCPKG" >> "$GITHUB_ENV" + echo "HADDOCK=$HADDOCK" >> "$GITHUB_ENV" + env: + HCKIND: ${{ matrix.compilerKind }} + HCNAME: ${{ matrix.compiler }} + HCVER: ${{ matrix.compilerVersion }} + - name: Set PATH and environment variables + run: | + echo "$HOME/.cabal/bin" >> $GITHUB_PATH + echo "LANG=C.UTF-8" >> "$GITHUB_ENV" + echo "CABAL_DIR=$HOME/.cabal" >> "$GITHUB_ENV" + echo "CABAL_CONFIG=$HOME/.cabal/config" >> "$GITHUB_ENV" + HCNUMVER=$(${HC} --numeric-version|perl -ne '/^(\d+)\.(\d+)\.(\d+)(\.(\d+))?$/; print(10000 * $1 + 100 * $2 + ($3 == 0 ? $5 != 1 : $3))') + echo "HCNUMVER=$HCNUMVER" >> "$GITHUB_ENV" + echo "ARG_TESTS=--enable-tests" >> "$GITHUB_ENV" + echo "ARG_BENCH=--enable-benchmarks" >> "$GITHUB_ENV" + echo "HEADHACKAGE=false" >> "$GITHUB_ENV" + echo "ARG_COMPILER=--$HCKIND --with-compiler=$HC" >> "$GITHUB_ENV" + env: + HCKIND: ${{ matrix.compilerKind }} + HCNAME: ${{ matrix.compiler }} + HCVER: ${{ matrix.compilerVersion }} + - name: env + run: | + env + - name: write cabal config + run: | + mkdir -p $CABAL_DIR + cat >> $CABAL_CONFIG <> $CABAL_CONFIG < cabal-plan.xz + echo 'f62ccb2971567a5f638f2005ad3173dba14693a45154c1508645c52289714cb2 cabal-plan.xz' | sha256sum -c - + xz -d < cabal-plan.xz > $HOME/.cabal/bin/cabal-plan + rm -f cabal-plan.xz + chmod a+x $HOME/.cabal/bin/cabal-plan + cabal-plan --version + - name: checkout + uses: actions/checkout@v4 + with: + path: source + - name: initial cabal.project for sdist + run: | + touch cabal.project + echo "packages: $GITHUB_WORKSPACE/source/servant" >> cabal.project + echo "packages: $GITHUB_WORKSPACE/source/servant-client" >> cabal.project + cat cabal.project + - name: sdist + run: | + mkdir -p sdist + $CABAL sdist all --output-dir $GITHUB_WORKSPACE/sdist + - name: unpack + run: | + mkdir -p unpacked + find sdist -maxdepth 1 -type f -name '*.tar.gz' -exec tar -C $GITHUB_WORKSPACE/unpacked -xzvf {} \; + - name: generate cabal.project + run: | + PKGDIR_servant="$(find "$GITHUB_WORKSPACE/unpacked" -maxdepth 1 -type d -regex '.*/servant-[0-9.]*')" + echo "PKGDIR_servant=${PKGDIR_servant}" >> "$GITHUB_ENV" + PKGDIR_servant_client="$(find "$GITHUB_WORKSPACE/unpacked" -maxdepth 1 -type d -regex '.*/servant-client-[0-9.]*')" + echo "PKGDIR_servant_client=${PKGDIR_servant_client}" >> "$GITHUB_ENV" + rm -f cabal.project cabal.project.local + touch cabal.project + touch cabal.project.local + echo "packages: ${PKGDIR_servant}" >> cabal.project + echo "packages: ${PKGDIR_servant_client}" >> cabal.project + if [ $((HCNUMVER >= 80200)) -ne 0 ] ; then echo "package servant" >> cabal.project ; fi + if [ $((HCNUMVER >= 80200)) -ne 0 ] ; then echo " ghc-options: -Werror=missing-methods" >> cabal.project ; fi + if [ $((HCNUMVER >= 80200)) -ne 0 ] ; then echo "package servant-client" >> cabal.project ; fi + if [ $((HCNUMVER >= 80200)) -ne 0 ] ; then echo " ghc-options: -Werror=missing-methods" >> cabal.project ; fi + cat >> cabal.project <> cabal.project.local + cat cabal.project + cat cabal.project.local + - name: dump install plan + run: | + $CABAL v2-build $ARG_COMPILER $ARG_TESTS $ARG_BENCH --dry-run all + cabal-plan + - name: restore cache + uses: actions/cache/restore@v4 + with: + key: ${{ runner.os }}-${{ matrix.compiler }}-${{ github.sha }} + path: ~/.cabal/store + restore-keys: ${{ runner.os }}-${{ matrix.compiler }}- + - name: install dependencies + run: | + $CABAL v2-build $ARG_COMPILER --disable-tests --disable-benchmarks --dependencies-only -j2 all + $CABAL v2-build $ARG_COMPILER $ARG_TESTS $ARG_BENCH --dependencies-only -j2 all + - name: build w/o tests + run: | + $CABAL v2-build $ARG_COMPILER --disable-tests --disable-benchmarks all + - name: build + run: | + $CABAL v2-build $ARG_COMPILER $ARG_TESTS $ARG_BENCH all --write-ghc-environment-files=always + - name: tests + run: | + $CABAL v2-test $ARG_COMPILER $ARG_TESTS $ARG_BENCH all --test-show-details=direct + - name: cabal check + run: | + cd ${PKGDIR_servant} || false + ${CABAL} -vnormal check + cd ${PKGDIR_servant_client} || false + ${CABAL} -vnormal check + - name: haddock + run: | + $CABAL v2-haddock --disable-documentation --haddock-all $ARG_COMPILER --with-haddock $HADDOCK $ARG_TESTS $ARG_BENCH all + - name: unconstrained build + run: | + rm -f cabal.project.local + $CABAL v2-build $ARG_COMPILER --disable-tests --disable-benchmarks all + - name: save cache + if: always() + uses: actions/cache/save@v4 + with: + key: ${{ runner.os }}-${{ matrix.compiler }}-${{ github.sha }} + path: ~/.cabal/store diff --git a/fixtures/conditionals.project b/fixtures/conditionals.project new file mode 100644 index 00000000..99fad6b4 --- /dev/null +++ b/fixtures/conditionals.project @@ -0,0 +1,3 @@ +packages: servant +if impl(ghc >=8.6) + packages: servant-client diff --git a/haskell-ci.cabal b/haskell-ci.cabal index edd2535d..37de0c40 100644 --- a/haskell-ci.cabal +++ b/haskell-ci.cabal @@ -1,6 +1,6 @@ cabal-version: 2.2 name: haskell-ci -version: 0.19.20250605 +version: 0.19.20250630 synopsis: Haskell CI script generator description: Script generator (@haskell-ci@) for @@ -176,7 +176,7 @@ library haskell-ci-internal , attoparsec ^>=0.14.1 , base-compat ^>=0.14.0 , base16-bytestring ^>=1.0.1.0 - , cabal-install-parsers ^>=0.6.3 + , cabal-install-parsers ^>=0.6.4 , cryptohash-sha256 ^>=0.11.101.0 , exceptions ^>=0.10.0 , generic-lens-lite ^>=0.1 diff --git a/src/HaskellCI.hs b/src/HaskellCI.hs index 6a3058ee..6c45e52a 100644 --- a/src/HaskellCI.hs +++ b/src/HaskellCI.hs @@ -35,6 +35,7 @@ import System.Process (readProcessWithExitCode) import Distribution.PackageDescription (GenericPackageDescription, package, packageDescription, testedWith) import Distribution.Text +import Distribution.Types.CondTree (CondBranch (..), CondTree (..)) import Distribution.Version import qualified Data.ByteString as BS @@ -360,13 +361,38 @@ getCabalFiles -> m (Project URI Void (FilePath, GenericPackageDescription)) getCabalFiles InputTypeProject path = do contents <- liftIO $ BS.readFile path - prj0 <- either (putStrLnErr . renderParseError) return $ parseProject path contents - prj1 <- either (putStrLnErr . renderResolveError) return =<< liftIO (resolveProject path prj0) + prj0 <- either (putStrLnErr . renderParseError) return $ parseProjectWithConditionals path contents + let prj0' :: Project Void String String + prj0' = simplifyProject prj0 + prj1 <- either (putStrLnErr . renderResolveError) return =<< liftIO (resolveProject path prj0') either (putStrLnErr . renderParseError) return =<< liftIO (readPackagesOfProject prj1) getCabalFiles InputTypePackage path = do e <- liftIO $ readPackagesOfProject (emptyProject & field @"prjPackages" .~ [path]) either (putStrLnErr . renderParseError) return e +simplifyProject :: CondTree c d (Project Void String String) -> Project Void String String +simplifyProject (CondNode a _ ifs) = + foldl (<<>>) a $ map simplifyProject' ifs + +simplifyProject' :: CondBranch v d (Project Void String String) -> Project Void String String +simplifyProject' (CondBranch _ t Nothing) = simplifyProject t +simplifyProject' (CondBranch _ t (Just e)) = simplifyProject t <<>> simplifyProject e + +-- TODO: we preserve only top-level structure and packages specs. +(<<>>) :: Project pkg opt uri -> Project pkg opt uri -> Project pkg opt uri +x <<>> y = Project + { prjPackages = prjPackages x <> prjPackages y + , prjOptPackages = prjOptPackages x <> prjOptPackages y + , prjUriPackages = prjUriPackages x <> prjUriPackages y + , prjConstraints = prjConstraints x + , prjAllowNewer = prjAllowNewer x + , prjReorderGoals = prjReorderGoals x + , prjMaxBackjumps = prjMaxBackjumps x + , prjOptimization = prjOptimization x + , prjSourceRepos = prjSourceRepos x + , prjOtherFields = prjOtherFields x + } + ------------------------------------------------------------------------------- -- Config ------------------------------------------------------------------------------- diff --git a/test/Tests.hs b/test/Tests.hs index 6017158d..f23048ba 100644 --- a/test/Tests.hs +++ b/test/Tests.hs @@ -34,6 +34,7 @@ main = do , fixtureGoldenTest "enabled-jobs" , fixtureGoldenTest "doctest" , fixtureGoldenTest "doctest-version" + , fixtureGoldenTest "conditionals" , testGroup "copy-fields" [ fixtureGoldenTest "copy-fields-all" , fixtureGoldenTest "copy-fields-some"