diff --git a/Cabal-QuickCheck/src/Test/QuickCheck/Instances/Cabal.hs b/Cabal-QuickCheck/src/Test/QuickCheck/Instances/Cabal.hs index 0cdff2283e9..73714c218c9 100644 --- a/Cabal-QuickCheck/src/Test/QuickCheck/Instances/Cabal.hs +++ b/Cabal-QuickCheck/src/Test/QuickCheck/Instances/Cabal.hs @@ -480,8 +480,10 @@ shortListOf1 bound gen = sized $ \n -> do k <- choose (1, 1 `max` ((n `div` 2) `min` bound)) vectorOf k gen +-- | The parsec parser reads a single-value field as one token, stopping at a +-- comma, whereas the legacy parser took the rest of the line. arbitraryShortToken :: Gen String -arbitraryShortToken = arbitraryShortStringWithout "{}[]" +arbitraryShortToken = arbitraryShortStringWithout "{}[]," arbitraryShortPath :: Gen String arbitraryShortPath = arbitraryShortStringWithout "{}[],<>:|*?" `suchThat` (not . winDevice) diff --git a/Cabal-syntax/src/Distribution/FieldGrammar/Newtypes.hs b/Cabal-syntax/src/Distribution/FieldGrammar/Newtypes.hs index 52eaf4769cd..dce9abbafda 100644 --- a/Cabal-syntax/src/Distribution/FieldGrammar/Newtypes.hs +++ b/Cabal-syntax/src/Distribution/FieldGrammar/Newtypes.hs @@ -138,6 +138,12 @@ newtype List sep b a = List {_getList :: [a]} -- -- >>> :t alaList' FSep Token -- alaList' FSep Token :: [String] -> List FSep Token String +-- +-- >>> _getList <$> (eitherParsec "foo bar foo" :: Either String (List FSep Token String)) +-- Right ["foo","bar","foo"] +-- +-- >>> _getList <$> (eitherParsec "xL{4,IE-,eK<}fE?e" :: Either String (List FSep Token String)) +-- Right ["xL{4","IE-","eK<}fE?e"] alaList :: sep -> [a] -> List sep (Identity a) a alaList _ = List diff --git a/Cabal/src/Distribution/Simple/Compiler.hs b/Cabal/src/Distribution/Simple/Compiler.hs index fbf159f11a6..b33403958bb 100644 --- a/Cabal/src/Distribution/Simple/Compiler.hs +++ b/Cabal/src/Distribution/Simple/Compiler.hs @@ -1,4 +1,5 @@ {-# LANGUAGE DataKinds #-} +{-# LANGUAGE ViewPatterns #-} -- | -- Module : Distribution.Simple.Compiler @@ -357,9 +358,11 @@ intToOptimisationLevel i -- levels. For compilers that do not the level is just capped to the -- level they do support. data DebugInfoLevel - = NoDebugInfo + = -- | The default and disabled level. Disabled by @--disable-debug-info@ or @debug-info: False@. + NoDebugInfo | MinimalDebugInfo - | NormalDebugInfo + | -- | The enabled level when enabled by @--enable-debug-info@ or @debug-info: True@. + NormalDebugInfo | MaximalDebugInfo deriving (Bounded, Enum, Eq, Generic, Read, Show) @@ -373,8 +376,24 @@ instance Parsec DebugInfoLevel where parsecDebugInfoLevel :: CabalParsing m => m DebugInfoLevel parsecDebugInfoLevel = flagToDebugInfoLevel . pure <$> parsecToken +-- | Converts a string to a 'DebugInfoLevel'. The string can be either a boolean +-- or 0-based integer for that enum. Evaluates to the default of 'NoDebugInfo' +-- when there's no string or when the string is "False". When the string is +-- "True", the level is 'NormalDebugInfo'. These string comparisons are +-- case-insensitive. +-- +-- >>> [flagToDebugInfoLevel (Just $ show n) | n <- [0 .. 3]] +-- [NoDebugInfo,MinimalDebugInfo,NormalDebugInfo,MaximalDebugInfo] +-- +-- >>> nub $ flagToDebugInfoLevel . Just <$> ["0", "False", "false"] +-- [NoDebugInfo] +-- +-- >>> nub $ flagToDebugInfoLevel <$> Nothing : (Just <$> ["2", "True", "true"]) +-- [NormalDebugInfo] flagToDebugInfoLevel :: Maybe String -> DebugInfoLevel flagToDebugInfoLevel Nothing = NormalDebugInfo +flagToDebugInfoLevel (Just (fmap toLower -> "false")) = NoDebugInfo +flagToDebugInfoLevel (Just (fmap toLower -> "true")) = NormalDebugInfo flagToDebugInfoLevel (Just s) = case reads s of [(i, "")] | i >= fromEnum (minBound :: DebugInfoLevel) diff --git a/cabal-install/cabal-install.cabal b/cabal-install/cabal-install.cabal index 83628ab03d1..5d6400014cf 100644 --- a/cabal-install/cabal-install.cabal +++ b/cabal-install/cabal-install.cabal @@ -398,17 +398,20 @@ test-suite parser-tests type: exitcode-stdio-1.0 main-is: Tests.hs - hs-source-dirs: parser-tests + hs-source-dirs: parser-tests tests build-depends: , cabal-install + , Cabal-tree-diff , containers , directory , filepath , network-uri >= 2.6.2.0 && <2.7 , tasty >= 1.2.3 && <1.6 , tasty-hunit >= 0.10 + , tree-diff other-modules: Tests.ParserTests + UnitTests.Distribution.Client.TreeDiffInstances -- Tests to run with a limited stack and heap size -- The test suite name must be keep short cause a longer one diff --git a/cabal-install/parser-tests/Tests/ParserTests.hs b/cabal-install/parser-tests/Tests/ParserTests.hs index 33debe829f9..df2789c038d 100644 --- a/cabal-install/parser-tests/Tests/ParserTests.hs +++ b/cabal-install/parser-tests/Tests/ParserTests.hs @@ -24,7 +24,7 @@ import Distribution.Client.Targets (readUserConstraint) import Distribution.Client.Types.AllowNewer (AllowNewer (..), AllowOlder (..), RelaxDepMod (..), RelaxDepScope (..), RelaxDepSubject (..), RelaxDeps (..), RelaxedDep (..)) import Distribution.Client.Types.InstallMethod (InstallMethod (..)) import Distribution.Client.Types.OverwritePolicy (OverwritePolicy (..)) -import Distribution.Client.Types.Repo (LocalRepo (..), RemoteRepo (..), asPosixPath) +import Distribution.Client.Types.Repo (LocalRepo (..), RemoteRepo (..)) import Distribution.Client.Types.RepoName (RepoName (..)) import Distribution.Client.Types.SourceRepo import Distribution.Client.Types.WriteGhcEnvironmentFilesPolicy (WriteGhcEnvironmentFilesPolicy (..)) @@ -48,7 +48,6 @@ import Distribution.Solver.Types.Settings , ReorderGoals (..) , StrongFlags (..) ) -import Distribution.System (OS (..), buildOS) import Distribution.Types.CondTree (CondTree (..)) import Distribution.Types.Flag (mkFlagAssignment) import Distribution.Types.PackageId (PackageIdentifier (..)) @@ -62,18 +61,24 @@ import Distribution.Verbosity import GHC.Stack (HasCallStack) import Network.URI (parseURI) import System.Directory (canonicalizePath, doesFileExist) -import System.FilePath (()) +import System.FilePath (normalise, ()) import Prelude () import Test.Tasty (TestTree, testGroup) -import Test.Tasty.HUnit (Assertion, assertBool, assertEqual, testCase) +import Test.Tasty.HUnit (Assertion, assertBool, assertEqual, assertFailure, testCase) + +import Data.TreeDiff (ToExpr, ansiWlEditExpr, ediff) +import UnitTests.Distribution.Client.TreeDiffInstances () parserTests :: TestTree parserTests = testGroup "project files parsec tests" [ testCase "read packages" testPackages + , testCase "read packages glob" testPackagesGlob + , testCase "read packages comma separated" testPackagesCommaSeparated , testCase "read optional-packages" testOptionalPackages + , testCase "read optional-packages glob" testOptionalPackagesGlob , testCase "read extra-packages" testExtraPackages , testCase "read source-repository-package" testSourceRepoList , testCase "read project-config-build-only" testProjectConfigBuildOnly @@ -81,8 +86,10 @@ parserTests = , testCase "read install-dirs" testInstallDirs , testCase "read remote-repos" testRemoteRepos , testCase "read local-no-index-repos" testLocalNoIndexRepos + , testCase "read project-file-parser" testProjectFileParser , testCase "set explicit provenance" testProjectConfigProvenance , testCase "read project-config-local-packages" testProjectConfigLocalPackages + , testCase "read project-config-local-packages-empty-string" testProjectConfigLocalPackagesEmptyString , testCase "read project-config-all-packages" testProjectConfigAllPackages , testCase "read project-config-specific-packages" testProjectConfigSpecificPackages , testCase "test projectConfigAllPackages concatenation" testAllPackagesConcat @@ -98,16 +105,35 @@ parserTests = testPackages :: Assertion testPackages = do - let expected = [".", "packages/packages.cabal"] + let expected = [".", "packages/packages.cabal", "a", "b"] (config, legacy) <- readConfigDefault "packages" assertConfigEquals expected config legacy (projectPackages . snd . condTreeData) +testPackagesGlob :: Assertion +testPackagesGlob = do + let expected = ["*/*.cabal", "../{foo,bar}/"] + (config, legacy) <- readConfig "packages" "cabal.glob.project" + assertConfigEquals expected config legacy (projectPackages . snd . condTreeData) + +testPackagesCommaSeparated :: Assertion +testPackagesCommaSeparated = do + let expected = ["xL{4,IE-,eK<}fE?e"] + -- let expected = ["xL{4","IE-","eK<}fE?e"] + (config, legacy) <- readConfig "packages" "cabal.comma-separated.project" + assertConfigEquals expected config legacy (projectPackages . snd . condTreeData) + testOptionalPackages :: Assertion testOptionalPackages = do let expected = [".", "packages/packages.cabal"] (config, legacy) <- readConfigDefault "optional-packages" assertConfigEquals expected config legacy (projectPackagesOptional . snd . condTreeData) +testOptionalPackagesGlob :: Assertion +testOptionalPackagesGlob = do + let expected = ["*/*.cabal", "../{foo,bar}/"] + (config, legacy) <- readConfig "optional-packages" "cabal.glob.project" + assertConfigEquals expected config legacy (projectPackagesOptional . snd . condTreeData) + testSourceRepoList :: Assertion testSourceRepoList = do (config, legacy) <- readConfigDefault "source-repository-packages" @@ -298,26 +324,31 @@ testRemoteRepos = do testLocalNoIndexRepos :: Assertion testLocalNoIndexRepos = do (config, legacy) <- readConfigDefault "local-no-index-repos" - let actualLocalRepos = (fromNubList . projectConfigLocalNoIndexRepos . projectConfigShared . snd . condTreeData) config - assertBool "Expected LocalNoIndexRepos do not match parsed values" $ compareLists expected actualLocalRepos compareLocalRepos + let localRepos = fromNubList . projectConfigLocalNoIndexRepos . projectConfigShared . snd . condTreeData + assertBool "Expected LocalNoIndexRepos do not match parsed values" $ compareLists expected (localRepos config) compareLocalRepos + assertBool "Expected LocalNoIndexRepos do not match legacy parsed values" $ compareLists expected (localRepos legacy) compareLocalRepos assertConfigEquals mempty config legacy (projectConfigRemoteRepos . projectConfigShared . snd . condTreeData) where expected = [myRepository, mySecureRepository] myRepository = LocalRepo { localRepoName = RepoName "my-repository" - , localRepoPath = normalisePath "/absolute/path/to/directory" + , localRepoPath = normalise "/absolute/path/to/directory" , localRepoSharedCache = False } mySecureRepository = LocalRepo { localRepoName = RepoName "my-other-repository" - , localRepoPath = normalisePath "/another/path/to/repository" + , localRepoPath = normalise "/another/path/to/repository" , localRepoSharedCache = False } - normalisePath path = case buildOS of - Windows -> asPosixPath path - _ -> path + +-- | The parser is chosen before the project file is read, so neither parser +-- takes this field from the file. +testProjectFileParser :: Assertion +testProjectFileParser = do + (config, legacy) <- readConfigDefault "project-file-parser" + assertConfigEquals NoFlag config legacy (projectConfigProjectFileParser . projectConfigShared . snd . condTreeData) testProjectConfigProvenance :: Assertion testProjectConfigProvenance = do @@ -397,6 +428,18 @@ testProjectConfigLocalPackages = do packageConfigTestTestOptions = [toPathTemplate "--some-option", toPathTemplate "42"] packageConfigBenchmarkOptions = [toPathTemplate "--some-benchmark-option", toPathTemplate "--another-option"] +-- | The parsers differ on a field with an empty value. The legacy parser +-- passes the empty rest of the line to the option's reader and so sets the +-- field to the empty string. The parsec parser sees a field with no lines and +-- leaves it unset, which is the better behaviour. +testProjectConfigLocalPackagesEmptyString :: Assertion +testProjectConfigLocalPackagesEmptyString = do + (config, legacy) <- readConfigDiverging "project-config-local-packages" "cabal.empty-string.project" + assertEqual "Legacy parser sets the empty string" (toFlag (toPathTemplate "")) (field legacy) + assertEqual "Parsec parser leaves the field unset" NoFlag (field config) + where + field = packageConfigTestHumanLog . projectConfigLocalPackages . snd . condTreeData + testProjectConfigAllPackages :: Assertion testProjectConfigAllPackages = do (config, legacy) <- readConfigDefault "project-config-all-packages" @@ -563,8 +606,26 @@ verbosity = mkVerbosity defaultVerbosityHandles normal readConfigDefault :: FilePath -> IO (ProjectConfigSkeleton, ProjectConfigSkeleton) readConfigDefault testSubDir = readConfig testSubDir "cabal.project" +-- | Reads a project file with both parsers and, with the legacy parser as the +-- oracle, checks that the parsec parser agrees with it on the whole config +-- before the caller looks at any one field. readConfig :: FilePath -> FilePath -> IO (ProjectConfigSkeleton, ProjectConfigSkeleton) readConfig testSubDir projectFileName = do + (parsec, legacy) <- readConfigDiverging testSubDir projectFileName + assertEdiffEqual "Parsec parser disagrees with the legacy parser" legacy parsec + return (parsec, legacy) + +-- | Like 'assertEqual' but the failure shows a tree diff of the two values +-- instead of two 'show' dumps. +assertEdiffEqual :: (Eq a, ToExpr a, HasCallStack) => String -> a -> a -> Assertion +assertEdiffEqual msg expected actual = + unless (expected == actual) . assertFailure $ + unlines [msg, show (ansiWlEditExpr (ediff expected actual))] + +-- | Reads a project file with both parsers without checking that they agree, +-- for the fixtures where they are known to differ. +readConfigDiverging :: FilePath -> FilePath -> IO (ProjectConfigSkeleton, ProjectConfigSkeleton) +readConfigDiverging testSubDir projectFileName = do (TestDir testRootFp projectConfigFp distDirLayout) <- testDirInfo testSubDir projectFileName exists <- liftIO $ doesFileExist projectConfigFp assertBool ("projectConfig does not exist: " <> projectConfigFp) exists diff --git a/cabal-install/parser-tests/Tests/files/optional-packages/cabal.glob.project b/cabal-install/parser-tests/Tests/files/optional-packages/cabal.glob.project new file mode 100644 index 00000000000..fbd2bd4aafe --- /dev/null +++ b/cabal-install/parser-tests/Tests/files/optional-packages/cabal.glob.project @@ -0,0 +1,3 @@ +-- The glob pattern syntax example from the users guide. +-- https://github.com/haskell/cabal/blob/d8c1b6f68df0a736c32a15ae85db9ffaf002ecb8/doc/cabal-project-description-file.rst?plain=1#L146 +optional-packages: */*.cabal ../{foo,bar}/ diff --git a/cabal-install/parser-tests/Tests/files/packages/cabal.comma-separated.project b/cabal-install/parser-tests/Tests/files/packages/cabal.comma-separated.project new file mode 100644 index 00000000000..9b96d57edd5 --- /dev/null +++ b/cabal-install/parser-tests/Tests/files/packages/cabal.comma-separated.project @@ -0,0 +1,5 @@ +-- This example was generated for the property test "round trip packages" and +-- resulted in this reported difference: +-- projectPackages = [-"xL{4", -"IE-", -"eK<}fE?e", +"xL{4,IE-,eK<}fE?e"] +-- projectPackages = [-"7{u", -"{h", -"{=n}}}", +"7{u,{h,{=n}}}"] +packages: xL{4,IE-,eK<}fE?e diff --git a/cabal-install/parser-tests/Tests/files/packages/cabal.glob.project b/cabal-install/parser-tests/Tests/files/packages/cabal.glob.project new file mode 100644 index 00000000000..e66b0a05bab --- /dev/null +++ b/cabal-install/parser-tests/Tests/files/packages/cabal.glob.project @@ -0,0 +1,3 @@ +-- The glob pattern syntax example from the users guide. +-- https://github.com/haskell/cabal/blob/d8c1b6f68df0a736c32a15ae85db9ffaf002ecb8/doc/cabal-project-description-file.rst?plain=1#L146 +packages: */*.cabal ../{foo,bar}/ diff --git a/cabal-install/parser-tests/Tests/files/packages/cabal.project b/cabal-install/parser-tests/Tests/files/packages/cabal.project index 6d9d4728a55..2fec31f920a 100644 --- a/cabal-install/parser-tests/Tests/files/packages/cabal.project +++ b/cabal-install/parser-tests/Tests/files/packages/cabal.project @@ -1 +1,2 @@ packages: . packages/packages.cabal +packages: a,b diff --git a/cabal-install/parser-tests/Tests/files/project-config-local-packages/cabal.empty-string.project b/cabal-install/parser-tests/Tests/files/project-config-local-packages/cabal.empty-string.project new file mode 100644 index 00000000000..94079d00450 --- /dev/null +++ b/cabal-install/parser-tests/Tests/files/project-config-local-packages/cabal.empty-string.project @@ -0,0 +1 @@ +test-log: diff --git a/cabal-install/parser-tests/Tests/files/project-file-parser/cabal.project b/cabal-install/parser-tests/Tests/files/project-file-parser/cabal.project new file mode 100644 index 00000000000..4781da5b447 --- /dev/null +++ b/cabal-install/parser-tests/Tests/files/project-file-parser/cabal.project @@ -0,0 +1 @@ +project-file-parser: legacy diff --git a/cabal-install/src/Distribution/Client/Config.hs b/cabal-install/src/Distribution/Client/Config.hs index f62caaf35ad..9833c5952a2 100644 --- a/cabal-install/src/Distribution/Client/Config.hs +++ b/cabal-install/src/Distribution/Client/Config.hs @@ -94,6 +94,7 @@ import Distribution.Client.Types , RemoteRepo (..) , RepoName (..) , emptyRemoteRepo + , fileNoIndexURIPath , isRelaxDeps , unRepoName ) @@ -207,6 +208,7 @@ import Distribution.Simple.Utils , writeFileAtomic ) import Distribution.Solver.Types.ConstraintSource +import Distribution.System (buildOS) import Distribution.Utils.Path (getSymbolicPath, unsafeMakeSymbolicPath) import Distribution.Verbosity ( normal @@ -228,8 +230,7 @@ import System.Directory , renameFile ) import System.FilePath - ( normalise - , takeDirectory + ( takeDirectory , () ) import System.IO.Error @@ -1689,7 +1690,9 @@ postProcessRepo lineno reponameStr repo0 = do Left $ LocalRepo reponame - (normalise (uriPath uri)) + -- Native path, so a repository named here and in a project + -- file compares equal and shares one cache key. + (fileNoIndexURIPath buildOS uri) (uriFragment uri == "#shared-cache") _ -> do let repo = repo0{remoteRepoName = reponame} diff --git a/cabal-install/src/Distribution/Client/ProjectConfig/FieldGrammar.hs b/cabal-install/src/Distribution/Client/ProjectConfig/FieldGrammar.hs index 9ba089bca17..96e2f78d444 100644 --- a/cabal-install/src/Distribution/Client/ProjectConfig/FieldGrammar.hs +++ b/cabal-install/src/Distribution/Client/ProjectConfig/FieldGrammar.hs @@ -15,19 +15,24 @@ import Distribution.Client.CmdInstall.ClientInstallFlags (clientInstallFlagsGram import qualified Distribution.Client.ProjectConfig.Lens as L import Distribution.Client.ProjectConfig.Types (PackageConfig (..), ProjectConfig (..), ProjectConfigBuildOnly (..), ProjectConfigProvenance (..), ProjectConfigShared (..)) import Distribution.Client.Utils.Parsec +import qualified Distribution.Compat.CharParsing as P +import Distribution.Compat.Lens (Lens') import Distribution.Compat.Prelude import Distribution.FieldGrammar +import Distribution.Parsec (CabalParsing, Parsec (..), parsecHaskellString) +import Distribution.Pretty (Pretty (..)) import Distribution.Simple.Flag import Distribution.Simple.InstallDirs import Distribution.Solver.Types.ConstraintSource (ConstraintSource (..)) import Distribution.Solver.Types.ProjectConfigPath import Distribution.Solver.Types.Settings (PreferVersion (..)) import Distribution.Types.PackageVersionConstraint (PackageVersionConstraint (..)) +import qualified Text.PrettyPrint as PP projectConfigFieldGrammar :: ProjectConfigPath -> [String] -> ParsecFieldGrammar' ProjectConfig projectConfigFieldGrammar source knownPrograms = do - projectPackages <- monoidalFieldAla "packages" (alaList' FSep Token) L.projectPackages - projectPackagesOptional <- monoidalFieldAla "optional-packages" (alaList' FSep Token) L.projectPackagesOptional + projectPackages <- getPackageLocationTokens <$> monoidalField "packages" ignoredLens + projectPackagesOptional <- getPackageLocationTokens <$> monoidalField "optional-packages" ignoredLens let projectPackagesRepo = mempty projectPackagesNamed <- monoidalFieldAla "extra-packages" formatPackageVersionConstraints L.projectPackagesNamed projectConfigBuildOnly <- blurFieldGrammar L.projectConfigBuildOnly projectConfigBuildOnlyFieldGrammar @@ -38,6 +43,57 @@ projectConfigFieldGrammar source knownPrograms = do projectConfigLocalPackages <- blurFieldGrammar L.projectConfigLocalPackages (packageConfigFieldGrammar knownPrograms) pure ProjectConfig{..} +newtype PackageLocationTokens = PackageLocationTokens {getPackageLocationTokens :: [String]} + +instance Semigroup PackageLocationTokens where + PackageLocationTokens a <> PackageLocationTokens b = PackageLocationTokens (a <> b) + +instance Monoid PackageLocationTokens where + mempty = PackageLocationTokens mempty + +instance Parsec PackageLocationTokens where + parsec = PackageLocationTokens <$> parseSep (Proxy :: Proxy FSep) parsePackageLocationTokenQ + +instance Pretty PackageLocationTokens where + pretty = prettySep (Proxy :: Proxy FSep) . map (PP.text . renderPackageLocationToken) . getPackageLocationTokens + +ignoredLens :: Lens' ProjectConfig PackageLocationTokens +ignoredLens f s = s <$ f mempty + +-- | This matches legacy parsing for @packages@ and @optional-packages@: +-- supports quoted strings, and for unquoted tokens allows commas only inside +-- balanced braces (e.g. ../{foo,bar}/). +parsePackageLocationTokenQ :: CabalParsing m => m String +parsePackageLocationTokenQ = parsecHaskellString <|> parsePackageLocationToken + where + parsePackageLocationToken = concat <$> some outerTerm + outerTerm = outerToken <|> braces innerTerm + innerTerm = concat <$> many (innerToken <|> braces innerTerm) + outerToken = P.munch1 outerChar + innerToken = P.munch1 innerChar + outerChar c = not (isSpace c || c == '{' || c == '}' || c == ',') + innerChar c = not (isSpace c || c == '{' || c == '}') + braces p = ("{" <>) . (<> "}") <$> P.between (P.char '{') (P.char '}') p + +renderPackageLocationToken :: String -> String +renderPackageLocationToken s + | needsQuoting = show s + | otherwise = s + where + needsQuoting = + not (ok 0 s) + || s == "." -- . on its own on a line has special meaning + || take 2 s == "--" -- on its own line is comment syntax + ok :: Int -> String -> Bool + ok n [] = n == 0 + ok _ ('"' : _) = False + ok n ('{' : cs) = ok (n + 1) cs + ok n ('}' : cs) = ok (n - 1) cs + ok n (',' : cs) = (n > 0) && ok n cs + ok _ (c : _) + | isSpace c = False + ok n (_ : cs) = ok n cs + formatPackageVersionConstraints :: [PackageVersionConstraint] -> List CommaVCat (Identity PackageVersionConstraint) PackageVersionConstraint formatPackageVersionConstraints = alaList CommaVCat diff --git a/cabal-install/src/Distribution/Client/ProjectConfig/Legacy.hs b/cabal-install/src/Distribution/Client/ProjectConfig/Legacy.hs index 0d5cb633646..54c92beb0a7 100644 --- a/cabal-install/src/Distribution/Client/ProjectConfig/Legacy.hs +++ b/cabal-install/src/Distribution/Client/ProjectConfig/Legacy.hs @@ -1265,6 +1265,35 @@ showLegacyProjectConfig config = -- but requires re-work of how we annotate provenance. constraintSrc = ConstraintSourceProjectConfig nullProjectConfigPath +-- | +-- >>> parseLegacyConvert projectPackages "packages" "foo" +-- ParseOk [] ["foo"] +-- +-- >>> parseLegacyConvert projectPackages "packages" "xL{4,IE-,eK<}fE?e" +-- ParseOk [] ["xL{4,IE-,eK<}fE?e"] +-- +-- >>> parseLegacyConvert projectPackages "packages" "7{u,{h,{=n}}}" +-- ParseOk [] ["7{u,{h,{=n}}}"] +-- +-- A top-level package field lands in the local packages, not in all packages. +-- +-- >>> parseLegacyConvert (packageConfigTestHumanLog . projectConfigAllPackages) "test-log" "foo" +-- ParseOk [] (Last {getLast = Nothing}) +-- +-- >>> parseLegacyConvert (packageConfigTestHumanLog . projectConfigLocalPackages) "test-log" "foo" +-- ParseOk [] (Last {getLast = Just "foo"}) +-- +-- An empty value sets the field to the empty string, where the parsec parser +-- would leave it unset, see 'Distribution.Client.ProjectConfig.Parsec.parseProjectConfig'. +-- +-- >>> parseLegacyConvert (packageConfigTestHumanLog . projectConfigLocalPackages) "test-log" "" +-- ParseOk [] (Last {getLast = Just ""}) +-- +-- >>> parseLegacyConvert (packageConfigTestHumanLog . projectConfigLocalPackages) "test-log" " " +-- ParseOk [] (Last {getLast = Just ""}) +-- +-- >>> parseLegacyConvert (packageConfigHaddockHtmlLocation . projectConfigLocalPackages) "haddock-html-location" "" +-- ParseOk [] (Last {getLast = Just ""}) legacyProjectConfigFieldDescrs :: ConstraintSource -> [FieldDescr LegacyProjectConfig] legacyProjectConfigFieldDescrs constraintSrc = [ newLineListField @@ -1480,6 +1509,9 @@ legacySharedConfigFieldDescrs constraintSrc = , liftFields legacyProjectFlags (\flags conf -> conf{legacyProjectFlags = flags}) + -- The parser is chosen before the project file is read, so this + -- field could have no effect. Leave it unrecognised, as parsec does. + . filter ((/= "project-file-parser") . ParseUtils.fieldName) . commandOptionsToFields $ projectFlagsOptions ParseArgs , [liftField legacyMultiRepl (\flags conf -> conf{legacyMultiRepl = flags}) (commandOptionToField multiReplOption)] @@ -2054,3 +2086,15 @@ showTokenQ "" = Disp.empty showTokenQ x@('-' : '-' : _) = Disp.text (show x) showTokenQ x@['.'] = Disp.text (show x) showTokenQ x = showToken x + +-- $setup +-- >>> import Distribution.Utils.Generic (toUTF8BS) +-- +-- Parses a project file of one field, going through the lexer as a real +-- project file would. +-- +-- >>> :{ +-- parseLegacyConvert :: (ProjectConfig -> a) -> String -> String -> ParseResult a +-- parseLegacyConvert f field s = (f . convertLegacyProjectConfig) <$> +-- parseLegacyProjectConfig "" (toUTF8BS (field ++ ": " ++ s)) +-- :} diff --git a/cabal-install/src/Distribution/Client/ProjectConfig/Parsec.hs b/cabal-install/src/Distribution/Client/ProjectConfig/Parsec.hs index 7893e03300a..01f912bd909 100644 --- a/cabal-install/src/Distribution/Client/ProjectConfig/Parsec.hs +++ b/cabal-install/src/Distribution/Client/ProjectConfig/Parsec.hs @@ -4,6 +4,7 @@ module Distribution.Client.ProjectConfig.Parsec ( -- * Package configuration parseProject + , parseProjectConfig , ProjectConfig (..) -- ** Parsing @@ -54,7 +55,7 @@ import qualified Data.Map.Strict as Map import qualified Data.Set as Set import Distribution.Client.Errors.Parser (ProjectFileSource (..)) import qualified Distribution.Compat.CharParsing as P -import Network.URI (URI, uriFragment, uriPath, uriScheme) +import Network.URI (URI, uriFragment, uriScheme) import System.Directory (makeAbsolute) import System.FilePath (splitFileName) import qualified Text.Parsec @@ -174,18 +175,6 @@ parseProjectSkeleton cacheDir httpTransport verbosity projectDir source (Project parseImport :: Position -> [FieldLine Position] -> ParseResult ProjectFileSource FilePath parseImport pos lines' = runFieldParser pos (P.many P.anyChar) cabalSpec lines' - -- We want a normalized path for @fieldsToConfig@. This eventually surfaces - -- in solver rejection messages and build messages "this build was affected - -- by the following (project) config files" so we want all paths shown there - -- to be relative to the directory of the project, not relative to the file - -- they were imported from. - fieldsToConfig :: ProjectConfigPath -> [Field Position] -> ParseResult ProjectFileSource ProjectConfig - fieldsToConfig sourceConfigPath xs = do - let (fs, sectionGroups) = partitionFields xs - sections = concat sectionGroups - config <- parseFieldGrammarCheckingStanzas cabalSpec fs (projectConfigFieldGrammar sourceConfigPath (knownProgramNames programDb)) stanzas - config' <- view stateConfig <$> execStateT (goSections programDb sections) (SectionS config) - return config' modifiesCompiler :: ProjectConfig -> Bool modifiesCompiler pc = isSet projectConfigHcFlavor || isSet projectConfigHcPath || isSet projectConfigHcPkg where @@ -199,8 +188,75 @@ parseProjectSkeleton cacheDir httpTransport verbosity projectDir source (Project sanityWalkBranch :: CondBranch ConfVar ([(Maybe URI, ProjectConfigPath)], ProjectConfig) -> ParseResult ProjectFileSource () sanityWalkBranch (CondBranch _c t f) = traverse_ (sanityWalkPCS True) f >> sanityWalkPCS True t >> pure () +-- We want a normalized path for @fieldsToConfig@. This eventually surfaces +-- in solver rejection messages and build messages "this build was affected +-- by the following (project) config files" so we want all paths shown there +-- to be relative to the directory of the project, not relative to the file +-- they were imported from. +fieldsToConfig :: ProjectConfigPath -> [Field Position] -> ParseResult ProjectFileSource ProjectConfig +fieldsToConfig sourceConfigPath xs = do + let (fs, sectionGroups) = partitionFields xs + sections = concat sectionGroups + warnProjectFileParserField fs + config <- parseFieldGrammarCheckingStanzas cabalSpec (Map.delete projectFileParserField fs) (projectConfigFieldGrammar sourceConfigPath (knownProgramNames programDb)) stanzas + config' <- view stateConfig <$> execStateT (goSections programDb sections) (SectionS config) + return config' + where programDb = defaultProgramDb +projectFileParserField :: FieldName +projectFileParserField = "project-file-parser" + +-- | The parser is chosen before the project file is read, so this field can +-- have no effect in a project file. Say so, rather than warn of an unknown +-- field. +warnProjectFileParserField :: Fields Position -> ParseResult src () +warnProjectFileParserField fs = + for_ (Map.findWithDefault [] projectFileParserField fs) $ \field -> + parseWarning + (namelessFieldAnn field) + PWTOther + "The project-file-parser field has no effect in a project file, the parser is chosen before the file is read. Use --project-file-parser on the command line instead." + +-- | +-- >>> parseParsec projectPackages "packages" "foo" +-- ([],Right ["foo"]) +-- +-- >>> parseParsec projectPackages "packages" "xL{4,IE-,eK<}fE?e" +-- ([],Right ["xL{4,IE-,eK<}fE?e"]) +-- +-- >>> parseParsec projectPackages "packages" "7{u,{h,{=n}}}" +-- ([],Right ["7{u,{h,{=n}}}"]) +-- +-- >>> parseParsec projectPackages "packages" "" +-- ([],Right []) +-- +-- >>> parseParsec (packageConfigTestHumanLog . projectConfigLocalPackages) "test-log" "foo" +-- ([],Right (Last {getLast = Just "foo"})) +-- +-- An empty value leaves the field unset, where the legacy parser would set it +-- to the empty string, see 'Distribution.Client.ProjectConfig.Legacy.legacyProjectConfigFieldDescrs'. +-- +-- >>> parseParsec (packageConfigTestHumanLog . projectConfigLocalPackages) "test-log" "" +-- ([],Right (Last {getLast = Nothing})) +-- +-- >>> parseParsec (packageConfigTestHumanLog . projectConfigLocalPackages) "test-log" " " +-- ([],Right (Last {getLast = Nothing})) +-- +-- >>> parseParsec (packageConfigHaddockHtmlLocation . projectConfigLocalPackages) "haddock-html-location" "" +-- ([],Right (Last {getLast = Nothing})) +-- +-- A @project-file-parser@ field is dropped with a warning saying why. +-- +-- >>> :{ +-- let (warnings, result) = runParseResult $ parseProjectConfig "" (toUTF8BS "project-file-parser: legacy\n") +-- in ([m | PWarningWithSource _ (PWarning _ _ m) <- warnings], projectConfigProjectFileParser . projectConfigShared <$> result) +-- :} +-- (["The project-file-parser field has no effect in a project file, the parser is chosen before the file is read. Use --project-file-parser on the command line instead."],Right (Last {getLast = Nothing})) +parseProjectConfig :: FilePath -> BS.ByteString -> ParseResult ProjectFileSource ProjectConfig +parseProjectConfig rootConfig bs = + fieldsToConfig (ProjectConfigPath $ rootConfig :| []) =<< readPreprocessFields bs + startOfSection :: Position -> [SectionArg Position] -> Position -- The case where we have no args is the start of the section startOfSection defaultPos [] = defaultPos @@ -284,13 +340,21 @@ stanzas :: Set BS.ByteString stanzas = Set.fromList ["source-repository-package", "program-options", "program-locations", "repository", "package"] -- | Currently a duplicate of 'Distribution.Client.Config.postProcessRepo' but migrated to Parsec ParseResult. +-- +-- A @file+noindex:@ repository is local and its path is read back as a +-- native path with 'fileNoIndexURIPath', the reading direction. The legacy +-- printer writes that path with 'normaliseFileNoIndexURI', the writing +-- direction, so the two must stay inverses of each other for a project file +-- to round trip. postProcessRemoteRepo :: Position -> RemoteRepo -> ParseResult src (Either LocalRepo RemoteRepo) postProcessRemoteRepo pos repo = case uriScheme (remoteRepoURI repo) of -- TODO: check that there are no authority, query or fragment -- Note: the trailing colon is important "file+noindex:" -> do - let uri = normaliseFileNoIndexURI buildOS $ remoteRepoURI repo - return $ Left $ LocalRepo (remoteRepoName repo) (uriPath uri) (uriFragment uri == "#shared-cache") + let uri = remoteRepoURI repo + -- Native path, not the URI's POSIX-style one: the config file parser + -- stores native too, and the cache key hashes the spelling. + return $ Left $ LocalRepo (remoteRepoName repo) (fileNoIndexURIPath buildOS uri) (uriFragment uri == "#shared-cache") _ -> do when (remoteRepoKeyThreshold repo > length (remoteRepoRootKeys repo)) $ warning $ @@ -409,3 +473,15 @@ warnUnknownFields fieldName fieldLines = for_ fieldLines (\field -> parseWarning cabalSpec :: CabalSpecVersion cabalSpec = cabalSpecLatest + +-- $setup +-- >>> instance (Show a, Show b) => Show (ParseResult a b) where show = show . runParseResult +-- >>> import Distribution.Parsec.Warning (PWarning (..), PWarningWithSource (..)) +-- +-- Parses a project file of one field, going through the lexer as a real +-- project file would. +-- +-- >>> :{ +-- parseParsec :: (ProjectConfig -> a) -> String -> String -> ParseResult ProjectFileSource a +-- parseParsec f field s = f <$> parseProjectConfig "" (toUTF8BS (field ++ ": " ++ s)) +-- :} diff --git a/cabal-install/src/Distribution/Client/Types/Repo.hs b/cabal-install/src/Distribution/Client/Types/Repo.hs index f01bfa744bf..352936e17bb 100644 --- a/cabal-install/src/Distribution/Client/Types/Repo.hs +++ b/cabal-install/src/Distribution/Client/Types/Repo.hs @@ -21,6 +21,7 @@ module Distribution.Client.Types.Repo -- * Windows , asPosixPath , normaliseFileNoIndexURI + , fileNoIndexURIPath ) where import Distribution.Client.Compat.Prelude @@ -229,10 +230,23 @@ repoName (RepoSecure r _) = remoteRepoName r -- | When on Windows, we need to convert the paths in URIs to be POSIX-style. -- +-- This is the writing direction, from the native 'localRepoPath' of a +-- 'LocalRepo' to the @url@ of a @repository@ section. Use it when printing +-- or building a @file+noindex:@ URI. For the reading direction, from that URI +-- back to a native path, use 'fileNoIndexURIPath' instead. +-- -- >>> import Network.URI -- >>> normaliseFileNoIndexURI Windows (URI "file+noindex:" (Just nullURIAuth) "C:\\dev\\foo" "" "") -- file+noindex:C:/dev/foo -- +-- Elsewhere, and for other schemes, the URI is left as it is. +-- +-- >>> import Distribution.System (OS (..)) +-- >>> normaliseFileNoIndexURI Linux (URI "file+noindex:" Nothing "/dev/foo" "" "") +-- file+noindex:/dev/foo +-- >>> normaliseFileNoIndexURI Windows (URI "file:" Nothing "C:\\dev\\foo" "" "") +-- file:C:\dev\foo +-- -- Other formats of file paths are not understood by @network-uri@: -- -- >>> import Network.URI @@ -254,7 +268,54 @@ normaliseFileNoIndexURI os uri@(URI scheme _auth path query fragment) URI scheme Nothing (asPosixPath path) query fragment | otherwise = uri +-- | The path of a @file+noindex:@ URI as a native path, for the +-- 'localRepoPath' of a 'LocalRepo'. +-- +-- This is the reading direction, the inverse of 'normaliseFileNoIndexURI'. +-- Use it when parsing a @repository@ section, in the user config file and in +-- project files alike. +-- +-- The path is stored native rather than POSIX-style because: +-- +-- 1. It is a 'FilePath', like every other path in cabal-install. +-- +-- 2. 'localRepoCacheKey' hashes its spelling to name a cache directory, so +-- two spellings of one directory would get two caches. +-- +-- 3. A repository given in both the config file and a project file must +-- compare equal to be deduplicated. +-- +-- Reading with 'normaliseFileNoIndexURI' instead would keep the URI's +-- forward slashes on Windows, which breaks 2 and 3 and makes 1 the odd one +-- out. +-- +-- On Windows the path is normalised to backslashes, whether it was written +-- POSIX-style by 'normaliseFileNoIndexURI' or with backslashes by hand. +-- +-- >>> import Network.URI +-- >>> import Distribution.System (OS (..)) +-- >>> fileNoIndexURIPath Windows (URI "file+noindex:" Nothing "C:/dev/foo" "" "") +-- "C:\\dev\\foo" +-- >>> fileNoIndexURIPath Windows (URI "file+noindex:" Nothing "C:\\dev\\foo" "" "") +-- "C:\\dev\\foo" +-- >>> fileNoIndexURIPath Linux (URI "file+noindex:" Nothing "/dev/foo" "" "") +-- "/dev/foo" +-- +-- Writing a native path and reading it back gives the native path again. +-- +-- >>> let uri = normaliseFileNoIndexURI Windows (URI "file+noindex:" Nothing "C:\\dev\\foo" "" "") +-- >>> (uri, fileNoIndexURIPath Windows uri) +-- (file+noindex:C:/dev/foo,"C:\\dev\\foo") +fileNoIndexURIPath :: OS -> URI -> FilePath +fileNoIndexURIPath Windows = Windows.normalise . uriPath +fileNoIndexURIPath _ = Posix.normalise . uriPath + -- | Convert a path to POSIX-style. +-- +-- >>> asPosixPath "C:\\dev\\foo" +-- "C:/dev/foo" +-- >>> asPosixPath "/dev/foo" +-- "/dev/foo" asPosixPath :: FilePath -> FilePath asPosixPath p = -- We don't use 'isPathSeparator' because @Windows.isPathSeparator diff --git a/cabal-install/src/Distribution/Client/Utils/Newtypes.hs b/cabal-install/src/Distribution/Client/Utils/Newtypes.hs index 6bde1e986d1..2dc2393703c 100644 --- a/cabal-install/src/Distribution/Client/Utils/Newtypes.hs +++ b/cabal-install/src/Distribution/Client/Utils/Newtypes.hs @@ -80,8 +80,16 @@ newtype MaxBackjumps = MaxBackjumps {getMaxBackjumps :: Int} instance Parsec MaxBackjumps where parsec = parseMaxBackjumps +-- | A negative number means unlimited backtracking, the same as the +-- @--max-backjumps@ option allows. +-- +-- >>> getMaxBackjumps <$> eitherParsec "4000" +-- Right 4000 +-- +-- >>> getMaxBackjumps <$> eitherParsec "-1" +-- Right (-1) parseMaxBackjumps :: CabalParsing m => m MaxBackjumps -parseMaxBackjumps = MaxBackjumps <$> integral +parseMaxBackjumps = MaxBackjumps <$> signedIntegral newtype AllowNewerNT = AllowNewerNT {getAllowNewerNT :: Maybe AllowNewer} diff --git a/cabal-install/tests/UnitTests/Distribution/Client/ArbitraryInstances.hs b/cabal-install/tests/UnitTests/Distribution/Client/ArbitraryInstances.hs index 50f41e3eff1..82107f37642 100644 --- a/cabal-install/tests/UnitTests/Distribution/Client/ArbitraryInstances.hs +++ b/cabal-install/tests/UnitTests/Distribution/Client/ArbitraryInstances.hs @@ -146,17 +146,22 @@ newtype ShortToken = ShortToken {getShortToken :: String} instance Arbitrary ShortToken where arbitrary = ShortToken - <$> ( shortListOf1 5 (choose ('#', '~')) - `suchThat` all (`notElem` "{}") - `suchThat` (not . ("[]" `isPrefixOf`)) - ) + <$> (shortListOf1 5 (choose ('#', '~')) `suchThat` isShortToken) -- TODO: [code cleanup] need to replace parseHaskellString impl to stop -- accepting Haskell list syntax [], ['a'] etc, just allow String syntax. -- Workaround, don't generate [] as this does not round trip. + -- Shrinking a 'Char' can produce a space, so filter shrinks back to what + -- the generator would have produced. shrink (ShortToken cs) = - [ShortToken cs' | cs' <- shrink cs, not (null cs')] + [ShortToken cs' | cs' <- shrink cs, not (null cs'), isShortToken cs'] + +-- | The strings that the 'ShortToken' generator produces. +isShortToken :: String -> Bool +isShortToken cs = + all (\c -> c >= '#' && c <= '~' && c `notElem` "{},") cs + && not ("[]" `isPrefixOf` cs) arbitraryShortToken :: Gen String arbitraryShortToken = getShortToken <$> arbitrary diff --git a/cabal-install/tests/UnitTests/Distribution/Client/ProjectConfig.hs b/cabal-install/tests/UnitTests/Distribution/Client/ProjectConfig.hs index 7c9a5817c5e..d3f8c03d413 100644 --- a/cabal-install/tests/UnitTests/Distribution/Client/ProjectConfig.hs +++ b/cabal-install/tests/UnitTests/Distribution/Client/ProjectConfig.hs @@ -17,7 +17,7 @@ import System.Directory (canonicalizePath, withCurrentDirectory) import System.FilePath import System.IO.Unsafe (unsafePerformIO) -import Distribution.Deprecated.ParseUtils +import Distribution.Deprecated.ParseUtils (ParseResult (..)) import qualified Distribution.Deprecated.ReadP as Parse import Distribution.Package @@ -48,6 +48,7 @@ import Distribution.Solver.Types.Settings import Distribution.Client.ProjectConfig import Distribution.Client.ProjectConfig.Legacy +import Distribution.Client.ProjectConfig.Parsec import UnitTests.Distribution.Client.ArbitraryInstances import UnitTests.Distribution.Client.TreeDiffInstances () @@ -78,12 +79,12 @@ tests = ] , testGroup "ProjectConfig printing/parsing round trip" - [ testProperty "packages" prop_roundtrip_printparse_packages - , testProperty "buildonly" prop_roundtrip_printparse_buildonly - , testProperty "shared" prop_roundtrip_printparse_shared - , testProperty "local" prop_roundtrip_printparse_local - , testProperty "specific" prop_roundtrip_printparse_specific - , testProperty "all" prop_roundtrip_printparse_all + [ testProperty "round trip packages" prop_roundtrip_printparse_packages + , testProperty "round trip buildonly" prop_roundtrip_printparse_buildonly + , testProperty "round trip shared" prop_roundtrip_printparse_shared + , testProperty "round trip local" prop_roundtrip_printparse_local + , testProperty "round trip specific" prop_roundtrip_printparse_specific + , testProperty "round trip all" prop_roundtrip_printparse_all ] , testGetProjectRootUsability , testFindProjectRoot @@ -257,16 +258,32 @@ prop_roundtrip_legacytypes_specific config = -- Round trip: printing and parsing config -- +-- | Prints with the legacy printer and parses with both parsers, each of +-- which must give back the config. The legacy parser is the oracle for the +-- parsec parser, so the generators avoid the inputs where the two are known +-- to differ, such as commas and empty values in single-value fields. roundtrip_printparse :: ProjectConfig -> Property roundtrip_printparse config = - case fmap convertLegacyProjectConfig (parseLegacyProjectConfig "unused" (toUTF8BS str)) of - ParseOk _ x -> - counterexample ("shown:\n" ++ str) $ - x `ediffEq` config{projectConfigProvenance = mempty} - ParseFailed err -> counterexample ("shown:\n" ++ str ++ "\nERROR: " ++ show err) False + countering $ + counterexample "parsec parser" parsecParsed + .&&. counterexample "legacy parser" legacyParsed where + parsecParsed = case runParseResult $ parseProjectConfig "unused" bs of + (_, Right result) -> result `roundTripped` config + (_, Left err) -> counterexample ("ERROR: " ++ show err) False + legacyParsed = case parseLegacyProjectConfig "unused" bs of + ParseOk _ result -> convertLegacyProjectConfig result `roundTripped` config + ParseFailed err -> counterexample ("ERROR: " ++ show err) False + roundTripped result expected = + ediffEq + result{projectConfigProvenance = mempty} + expected{projectConfigProvenance = mempty} + bs = toUTF8BS str str :: String str = showLegacyProjectConfig (convertToLegacyProjectConfig config) + countering = + counterexample ("shown:\n" ++ str) + . counterexample ("shown by line:\n" ++ unlines (map (\s -> "'" ++ s ++ "'") (lines str))) prop_roundtrip_printparse_all :: ProjectConfig -> Property prop_roundtrip_printparse_all config = @@ -588,7 +605,9 @@ instance Arbitrary ProjectConfigShared where projectConfigConfigFile <- arbitraryFlag arbitraryShortToken projectConfigProjectDir <- arbitraryFlag arbitraryShortToken projectConfigProjectFile <- arbitraryFlag arbitraryShortToken - projectConfigProjectFileParser <- arbitraryFlag arbitrary + -- The parser can only be chosen on the command line, not in a project + -- file, so the parsec parser never reads it back. + let projectConfigProjectFileParser = mempty projectConfigIgnoreProject <- arbitrary projectConfigHcFlavor <- arbitrary projectConfigHcPath <- arbitraryFlag arbitraryShortToken @@ -631,15 +650,15 @@ instance Arbitrary ProjectConfigShared where shrink ProjectConfigShared{..} = runShrinker $ ProjectConfigShared - <$> shrinker projectConfigDistDir - <*> shrinker projectConfigConfigFile - <*> shrinker projectConfigProjectDir - <*> shrinker projectConfigProjectFile + <$> shrinkerAla (fmap ShortToken) projectConfigDistDir + <*> shrinkerAla (fmap ShortToken) projectConfigConfigFile + <*> shrinkerAla (fmap ShortToken) projectConfigProjectDir + <*> shrinkerAla (fmap ShortToken) projectConfigProjectFile <*> shrinker projectConfigProjectFileParser <*> shrinker projectConfigIgnoreProject <*> shrinker projectConfigHcFlavor - <*> shrinkerAla (fmap NonEmpty) projectConfigHcPath - <*> shrinkerAla (fmap NonEmpty) projectConfigHcPkg + <*> shrinkerAla (fmap ShortToken) projectConfigHcPath + <*> shrinkerAla (fmap ShortToken) projectConfigHcPkg <*> shrinker projectConfigHaddockIndex <*> shrinker projectConfigInstallDirs <*> shrinker projectConfigPackageDBs @@ -647,7 +666,7 @@ instance Arbitrary ProjectConfigShared where <*> shrinker projectConfigLocalNoIndexRepos <*> shrinker projectConfigActiveRepos <*> shrinker projectConfigIndexState - <*> shrinker projectConfigStoreDir + <*> shrinkerAla (fmap ShortToken) projectConfigStoreDir <*> shrinkerPP preShrink_Constraints postShrink_Constraints projectConfigConstraints <*> shrinker projectConfigPreferences <*> shrinker projectConfigCabalVersion diff --git a/cabal-testsuite/PackageTests/ProjectConfig/DebugInfo/cabal.out b/cabal-testsuite/PackageTests/ProjectConfig/DebugInfo/cabal.out index 19b2a69fa0a..733b6d5a815 100644 --- a/cabal-testsuite/PackageTests/ProjectConfig/DebugInfo/cabal.out +++ b/cabal-testsuite/PackageTests/ProjectConfig/DebugInfo/cabal.out @@ -8,3 +8,6 @@ In order, the following would be built: # cabal build Warnings found while parsing the project file, cabal.boolean.project: - cabal.boolean.project:2:1: The field "debug-info" is specified more than once at positions 2:1, 3:1 +Build profile: -w ghc- -O1 +In order, the following would be built: + - debug-info-0 (lib) (first run) diff --git a/cabal-testsuite/PackageTests/ProjectConfig/DebugInfo/cabal.project b/cabal-testsuite/PackageTests/ProjectConfig/DebugInfo/cabal.project new file mode 100644 index 00000000000..ba2822430f8 --- /dev/null +++ b/cabal-testsuite/PackageTests/ProjectConfig/DebugInfo/cabal.project @@ -0,0 +1,2 @@ +optional-packages: . +debug-info: True diff --git a/cabal-testsuite/PackageTests/ProjectConfig/DebugInfo/cabal.test.hs b/cabal-testsuite/PackageTests/ProjectConfig/DebugInfo/cabal.test.hs index 66fb7f795f0..e917d59f6d3 100644 --- a/cabal-testsuite/PackageTests/ProjectConfig/DebugInfo/cabal.test.hs +++ b/cabal-testsuite/PackageTests/ProjectConfig/DebugInfo/cabal.test.hs @@ -6,7 +6,6 @@ main = cabalTest . recordMode RecordMarked $ do numeric <- cabal' "build" ["--project-file=cabal.numeric.project", "--dry-run"] assertOutputDoesNotContain errMsg numeric - boolean <- fails $ cabal' "build" ["--project-file=cabal.boolean.project", "--dry-run"] - -- TODO: When fixed, change this to assertOutputDoesNotContain. - assertOutputContains errMsg boolean + boolean <- cabal' "build" ["--project-file=cabal.boolean.project", "--dry-run"] + assertOutputDoesNotContain errMsg boolean pure () diff --git a/cabal-testsuite/PackageTests/ProjectConfig/ProjectFileParser/cabal.legacy.out b/cabal-testsuite/PackageTests/ProjectConfig/ProjectFileParser/cabal.legacy.out new file mode 100644 index 00000000000..2a64d409b7e --- /dev/null +++ b/cabal-testsuite/PackageTests/ProjectConfig/ProjectFileParser/cabal.legacy.out @@ -0,0 +1,7 @@ +# cabal build +Warnings found while parsing the project file, cabal.project: + - cabal.project: Unrecognized field 'project-file-parser' on line 2 +Resolving dependencies... +Build profile: -w ghc- -O1 +In order, the following would be built: + - project-file-parser-0 (lib) (first run) diff --git a/cabal-testsuite/PackageTests/ProjectConfig/ProjectFileParser/cabal.parsec.out b/cabal-testsuite/PackageTests/ProjectConfig/ProjectFileParser/cabal.parsec.out new file mode 100644 index 00000000000..41f6933239d --- /dev/null +++ b/cabal-testsuite/PackageTests/ProjectConfig/ProjectFileParser/cabal.parsec.out @@ -0,0 +1,7 @@ +# cabal build +Warnings found while parsing the project file, cabal.project: + - cabal.project:2:1: The project-file-parser field has no effect in a project file, the parser is chosen before the file is read. Use --project-file-parser on the command line instead. +Resolving dependencies... +Build profile: -w ghc- -O1 +In order, the following would be built: + - project-file-parser-0 (lib) (first run) diff --git a/cabal-testsuite/PackageTests/ProjectConfig/ProjectFileParser/cabal.project b/cabal-testsuite/PackageTests/ProjectConfig/ProjectFileParser/cabal.project new file mode 100644 index 00000000000..4811452c243 --- /dev/null +++ b/cabal-testsuite/PackageTests/ProjectConfig/ProjectFileParser/cabal.project @@ -0,0 +1,2 @@ +packages: . +project-file-parser: legacy diff --git a/cabal-testsuite/PackageTests/ProjectConfig/ProjectFileParser/cabal.test.hs b/cabal-testsuite/PackageTests/ProjectConfig/ProjectFileParser/cabal.test.hs new file mode 100644 index 00000000000..b0be8b3e62c --- /dev/null +++ b/cabal-testsuite/PackageTests/ProjectConfig/ProjectFileParser/cabal.test.hs @@ -0,0 +1,15 @@ +import Test.Cabal.Prelude + +-- The parser is chosen before the project file is read, so a +-- project-file-parser field in the project file can have no effect. Both +-- parsers warn about it, the parsec parser saying why. +main = do + let warning = "The project-file-parser field has no effect in a project file" + + cabalTest' "legacy" . recordMode RecordMarked $ do + legacy <- cabal' "build" ["--dry-run", "--project-file-parser=legacy"] + assertOutputContains "Unrecognized field 'project-file-parser'" legacy + + cabalTest' "parsec" . recordMode RecordMarked $ do + parsec <- cabal' "build" ["--dry-run", "--project-file-parser=parsec"] + assertOutputContains warning parsec diff --git a/cabal-testsuite/PackageTests/ProjectConfig/ProjectFileParser/project-file-parser.cabal b/cabal-testsuite/PackageTests/ProjectConfig/ProjectFileParser/project-file-parser.cabal new file mode 100644 index 00000000000..7bd7bbe9534 --- /dev/null +++ b/cabal-testsuite/PackageTests/ProjectConfig/ProjectFileParser/project-file-parser.cabal @@ -0,0 +1,7 @@ +cabal-version: 3.0 +name: project-file-parser +version: 0 +build-type: Simple + +library + default-language: Haskell2010 diff --git a/changelog.d/pr-12139.md b/changelog.d/pr-12139.md new file mode 100644 index 00000000000..9235f0e87c1 --- /dev/null +++ b/changelog.d/pr-12139.md @@ -0,0 +1,24 @@ +--- +synopsis: Fix parsec project parser roundtrip failures +packages: [Cabal, cabal-install] +prs: 12139 +--- + +The parsec parser for project files now accepts three things that only the +legacy parser accepted before: + +- `debug-info: True` and `debug-info: False`, case insensitively, as well as + the levels `0` to `3`. This also applies to `--enable-debug-info=True` on the + command line. +- A negative `max-backjumps`, such as `max-backjumps: -1` for unlimited + backtracking, matching `--max-backjumps`. +- Package locations in `packages` and `optional-packages` with commas inside + braces, such as `packages: ../{foo,bar}/`. + +A `project-file-parser` field in a project file can have no effect, as the +parser is chosen before the file is read. The legacy parser used to accept it +silently. Both parsers now warn about it, the parsec parser saying why and +pointing at the `--project-file-parser` option. + +The round trip property tests for project configuration now print with the +legacy printer and parse with the parsec parser. diff --git a/doc/cabal-project-description-file.rst b/doc/cabal-project-description-file.rst index 4b3858862f4..59f14c10fb6 100644 --- a/doc/cabal-project-description-file.rst +++ b/doc/cabal-project-description-file.rst @@ -445,7 +445,9 @@ Project options * ``fallback`` - the new parser using Parsec, but falling back to the old parser if it fails * ``compare`` - the new parser using Parsec, but comparing the results with the old parser - This option can only be specified from the command line. + This option can only be specified from the command line. The parser is + chosen before the project file is read, so a ``project-file-parser`` field + in a project file is ignored with a warning. .. option:: -z, --ignore-project @@ -1999,7 +2001,7 @@ Most users generally won't need these. The command line variant of this field is ``--solver=modular``. -.. cfg-field:: max-backjumps: nat +.. cfg-field:: max-backjumps: integer --max-backjumps=N :synopsis: Maximum number of solver backjumps.