From 7e728ead5425149b16409c86366c657aa79eabb6 Mon Sep 17 00:00:00 2001 From: Phil de Joux Date: Tue, 21 Jul 2026 14:19:13 -0400 Subject: [PATCH 01/41] Add parseProjectConfig --- .../Client/ProjectConfig/Parsec.hs | 30 +++++++++++-------- .../Distribution/Client/ProjectConfig.hs | 1 + 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/cabal-install/src/Distribution/Client/ProjectConfig/Parsec.hs b/cabal-install/src/Distribution/Client/ProjectConfig/Parsec.hs index 7893e03300a..c7477837a62 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 @@ -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,25 @@ 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 + config <- parseFieldGrammarCheckingStanzas cabalSpec fs (projectConfigFieldGrammar sourceConfigPath (knownProgramNames programDb)) stanzas + config' <- view stateConfig <$> execStateT (goSections programDb sections) (SectionS config) + return config' + where programDb = defaultProgramDb +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 diff --git a/cabal-install/tests/UnitTests/Distribution/Client/ProjectConfig.hs b/cabal-install/tests/UnitTests/Distribution/Client/ProjectConfig.hs index 7c9a5817c5e..5da6e11aad5 100644 --- a/cabal-install/tests/UnitTests/Distribution/Client/ProjectConfig.hs +++ b/cabal-install/tests/UnitTests/Distribution/Client/ProjectConfig.hs @@ -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 () From 869b9f1b466f7accae03c6e4f06b41d1b92d446f Mon Sep 17 00:00:00 2001 From: Phil de Joux Date: Tue, 21 Jul 2026 14:20:02 -0400 Subject: [PATCH 02/41] Update test to use parseProjectConfig --- .../tests/UnitTests/Distribution/Client/ProjectConfig.hs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/cabal-install/tests/UnitTests/Distribution/Client/ProjectConfig.hs b/cabal-install/tests/UnitTests/Distribution/Client/ProjectConfig.hs index 5da6e11aad5..3ea773224fe 100644 --- a/cabal-install/tests/UnitTests/Distribution/Client/ProjectConfig.hs +++ b/cabal-install/tests/UnitTests/Distribution/Client/ProjectConfig.hs @@ -17,7 +17,6 @@ import System.Directory (canonicalizePath, withCurrentDirectory) import System.FilePath import System.IO.Unsafe (unsafePerformIO) -import Distribution.Deprecated.ParseUtils import qualified Distribution.Deprecated.ReadP as Parse import Distribution.Package @@ -260,11 +259,11 @@ prop_roundtrip_legacytypes_specific config = roundtrip_printparse :: ProjectConfig -> Property roundtrip_printparse config = - case fmap convertLegacyProjectConfig (parseLegacyProjectConfig "unused" (toUTF8BS str)) of - ParseOk _ x -> + case runParseResult $ parseProjectConfig "unused" (toUTF8BS str) of + (_, Right x) -> counterexample ("shown:\n" ++ str) $ x `ediffEq` config{projectConfigProvenance = mempty} - ParseFailed err -> counterexample ("shown:\n" ++ str ++ "\nERROR: " ++ show err) False + (_, Left err) -> counterexample ("shown:\n" ++ str ++ "\nERROR: " ++ show err) False where str :: String str = showLegacyProjectConfig (convertToLegacyProjectConfig config) From ef0be71c32d0a4952754e6c4c67aedefa390df64 Mon Sep 17 00:00:00 2001 From: Phil de Joux Date: Tue, 21 Jul 2026 15:59:21 -0400 Subject: [PATCH 03/41] Add more specific test names --- .../UnitTests/Distribution/Client/ProjectConfig.hs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/cabal-install/tests/UnitTests/Distribution/Client/ProjectConfig.hs b/cabal-install/tests/UnitTests/Distribution/Client/ProjectConfig.hs index 3ea773224fe..101e2458778 100644 --- a/cabal-install/tests/UnitTests/Distribution/Client/ProjectConfig.hs +++ b/cabal-install/tests/UnitTests/Distribution/Client/ProjectConfig.hs @@ -78,12 +78,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 From 955ecf285eace0a6d266c382d75bf04d690e872f Mon Sep 17 00:00:00 2001 From: Phil de Joux Date: Tue, 21 Jul 2026 16:15:40 -0400 Subject: [PATCH 04/41] Clear provenance before comparison --- .../tests/UnitTests/Distribution/Client/ProjectConfig.hs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cabal-install/tests/UnitTests/Distribution/Client/ProjectConfig.hs b/cabal-install/tests/UnitTests/Distribution/Client/ProjectConfig.hs index 101e2458778..605d709b87d 100644 --- a/cabal-install/tests/UnitTests/Distribution/Client/ProjectConfig.hs +++ b/cabal-install/tests/UnitTests/Distribution/Client/ProjectConfig.hs @@ -260,9 +260,11 @@ prop_roundtrip_legacytypes_specific config = roundtrip_printparse :: ProjectConfig -> Property roundtrip_printparse config = case runParseResult $ parseProjectConfig "unused" (toUTF8BS str) of - (_, Right x) -> + (_, Right result) -> counterexample ("shown:\n" ++ str) $ - x `ediffEq` config{projectConfigProvenance = mempty} + ediffEq + result{projectConfigProvenance = mempty} + config{projectConfigProvenance = mempty} (_, Left err) -> counterexample ("shown:\n" ++ str ++ "\nERROR: " ++ show err) False where str :: String From cf998653866c8397bc832cc6b79e09a6754d9857 Mon Sep 17 00:00:00 2001 From: Phil de Joux Date: Wed, 22 Jul 2026 10:14:57 -0400 Subject: [PATCH 05/41] Add boolean parsing for debug info - Add haddocks for flagToDebugInfoLevel --- Cabal/src/Distribution/Simple/Compiler.hs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/Cabal/src/Distribution/Simple/Compiler.hs b/Cabal/src/Distribution/Simple/Compiler.hs index fbf159f11a6..ec714fb6385 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 @@ -373,8 +374,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) From 188ade3e21bd675ad1fa9a85fbb0cac5329b4fc9 Mon Sep 17 00:00:00 2001 From: Phil de Joux Date: Wed, 22 Jul 2026 10:20:09 -0400 Subject: [PATCH 06/41] Add haddocks to default and enabled levels --- Cabal/src/Distribution/Simple/Compiler.hs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Cabal/src/Distribution/Simple/Compiler.hs b/Cabal/src/Distribution/Simple/Compiler.hs index ec714fb6385..7ee10d85728 100644 --- a/Cabal/src/Distribution/Simple/Compiler.hs +++ b/Cabal/src/Distribution/Simple/Compiler.hs @@ -359,8 +359,10 @@ intToOptimisationLevel i -- level they do support. data DebugInfoLevel = NoDebugInfo + -- ^ The default and disabled level. Disabled by @--disable-debug-info@ or @debug-info: False@. | MinimalDebugInfo | NormalDebugInfo + -- ^ The enabled level when enabled by @--enable-debug-info@ or @debug-info: True@. | MaximalDebugInfo deriving (Bounded, Enum, Eq, Generic, Read, Show) From 09f7bbf45fc5032fe1b1a0f2c0812993b7b7c5d2 Mon Sep 17 00:00:00 2001 From: Phil de Joux Date: Wed, 22 Jul 2026 10:52:50 -0400 Subject: [PATCH 07/41] Add parsing debug-info test --- .../PackageTests/ProjectConfig/DebugInfo/cabal.project | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 cabal-testsuite/PackageTests/ProjectConfig/DebugInfo/cabal.project 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 From 1882dd7a37983b3c28a05d36cb0b5ed5de647b15 Mon Sep 17 00:00:00 2001 From: Phil de Joux Date: Tue, 28 Jul 2026 10:17:24 -0400 Subject: [PATCH 08/41] WIP --- Cabal-syntax/src/Distribution/FieldGrammar/Newtypes.hs | 6 ++++++ cabal-install/parser-tests/Tests/ParserTests.hs | 10 +++++++++- .../Tests/files/packages/cabal.comma-separated.project | 5 +++++ .../parser-tests/Tests/files/packages/cabal.project | 1 + .../src/Distribution/Client/ProjectConfig/Legacy.hs | 7 +++++++ 5 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 cabal-install/parser-tests/Tests/files/packages/cabal.comma-separated.project diff --git a/Cabal-syntax/src/Distribution/FieldGrammar/Newtypes.hs b/Cabal-syntax/src/Distribution/FieldGrammar/Newtypes.hs index 52eaf4769cd..0e411649302 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 +-- +-- >>> unpack' (alaList' FSep Token) <$> eitherParsec "foo bar foo" +-- Right ["foo","bar","foo"] +-- +-- >>> unpack' (alaList' FSep Token) <$> eitherParsec "xL{4,IE-,eK<}fE?e" +-- Right ["xL{4","IE-","eK<}fE?e"] alaList :: sep -> [a] -> List sep (Identity a) a alaList _ = List diff --git a/cabal-install/parser-tests/Tests/ParserTests.hs b/cabal-install/parser-tests/Tests/ParserTests.hs index 33debe829f9..65a1215d964 100644 --- a/cabal-install/parser-tests/Tests/ParserTests.hs +++ b/cabal-install/parser-tests/Tests/ParserTests.hs @@ -73,6 +73,7 @@ parserTests = testGroup "project files parsec tests" [ testCase "read packages" testPackages + , testCase "read packages comma separated" testPackagesCommaSeparated , testCase "read optional-packages" testOptionalPackages , testCase "read extra-packages" testExtraPackages , testCase "read source-repository-package" testSourceRepoList @@ -98,10 +99,17 @@ 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) +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"] 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.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/src/Distribution/Client/ProjectConfig/Legacy.hs b/cabal-install/src/Distribution/Client/ProjectConfig/Legacy.hs index 0d5cb633646..f6ea3daba70 100644 --- a/cabal-install/src/Distribution/Client/ProjectConfig/Legacy.hs +++ b/cabal-install/src/Distribution/Client/ProjectConfig/Legacy.hs @@ -1265,6 +1265,13 @@ showLegacyProjectConfig config = -- but requires re-work of how we annotate provenance. constraintSrc = ConstraintSourceProjectConfig nullProjectConfigPath +-- | +-- +-- :{ +-- fields <- readFields "packages: foo" +-- parseFieldsAndSections (legacyProjectConfigFieldDescrs ConstraintSourceUnknown fields) mempty +-- :} +-- legacyProjectConfigFieldDescrs :: ConstraintSource -> [FieldDescr LegacyProjectConfig] legacyProjectConfigFieldDescrs constraintSrc = [ newLineListField From 519683857fc6c26de3d2aa784bfe8649b9d4b83c Mon Sep 17 00:00:00 2001 From: Phil de Joux Date: Tue, 28 Jul 2026 10:46:58 -0400 Subject: [PATCH 09/41] Add doctests for parsing legacy packages --- .../Client/ProjectConfig/Legacy.hs | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/cabal-install/src/Distribution/Client/ProjectConfig/Legacy.hs b/cabal-install/src/Distribution/Client/ProjectConfig/Legacy.hs index f6ea3daba70..fc4371638c0 100644 --- a/cabal-install/src/Distribution/Client/ProjectConfig/Legacy.hs +++ b/cabal-install/src/Distribution/Client/ProjectConfig/Legacy.hs @@ -1240,6 +1240,13 @@ convertToLegacyPerPackageConfig PackageConfig{..} = parseLegacyProjectConfigFields :: ProjectConfigPath -> [ParseUtils.Field] -> ParseResult LegacyProjectConfig parseLegacyProjectConfigFields (ConstraintSourceProjectConfig -> constraintSrc) = + parseLegacyProjectConfigFieldsWithConstraintSource constraintSrc + +parseLegacyProjectConfigFieldsWithConstraintSource + :: ConstraintSource + -> [ParseUtils.Field] + -> ParseResult LegacyProjectConfig +parseLegacyProjectConfigFieldsWithConstraintSource constraintSrc = parseFieldsAndSections (legacyProjectConfigFieldDescrs constraintSrc) legacyPackageConfigSectionDescrs @@ -1266,12 +1273,14 @@ showLegacyProjectConfig config = constraintSrc = ConstraintSourceProjectConfig nullProjectConfigPath -- | +-- >>> parseLegacyPackages "foo" +-- ParseOk [] ["foo"] -- --- :{ --- fields <- readFields "packages: foo" --- parseFieldsAndSections (legacyProjectConfigFieldDescrs ConstraintSourceUnknown fields) mempty --- :} +-- >>> parseLegacyPackages "xL{4,IE-,eK<}fE?e" +-- ParseOk [] ["xL{4,IE-,eK<}fE?e"] -- +-- >>> parseLegacyPackages "7{u,{h,{=n}}}" +-- ParseOk [] ["7{u,{h,{=n}}}"] legacyProjectConfigFieldDescrs :: ConstraintSource -> [FieldDescr LegacyProjectConfig] legacyProjectConfigFieldDescrs constraintSrc = [ newLineListField @@ -2061,3 +2070,11 @@ showTokenQ "" = Disp.empty showTokenQ x@('-' : '-' : _) = Disp.text (show x) showTokenQ x@['.'] = Disp.text (show x) showTokenQ x = showToken x + +-- $setup +-- >>> :{ +-- parseLegacyPackages s = legacyPackages <$> +-- parseLegacyProjectConfigFieldsWithConstraintSource +-- ConstraintSourceUnknown +-- [ParseUtils.F 1 "packages" s] +-- :} From b2007219a1cf6f5f73dd41e3364d44684c345f26 Mon Sep 17 00:00:00 2001 From: Phil de Joux Date: Tue, 28 Jul 2026 11:06:26 -0400 Subject: [PATCH 10/41] Add packages glob parser test --- cabal-install/parser-tests/Tests/ParserTests.hs | 7 +++++++ .../parser-tests/Tests/files/packages/cabal.glob.project | 3 +++ 2 files changed, 10 insertions(+) create mode 100644 cabal-install/parser-tests/Tests/files/packages/cabal.glob.project diff --git a/cabal-install/parser-tests/Tests/ParserTests.hs b/cabal-install/parser-tests/Tests/ParserTests.hs index 65a1215d964..6ff00ea189a 100644 --- a/cabal-install/parser-tests/Tests/ParserTests.hs +++ b/cabal-install/parser-tests/Tests/ParserTests.hs @@ -73,6 +73,7 @@ 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 extra-packages" testExtraPackages @@ -103,6 +104,12 @@ testPackages = do (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"] 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}/ From 52ae3409eee623a895951682560ca813e95767a2 Mon Sep 17 00:00:00 2001 From: Phil de Joux Date: Tue, 28 Jul 2026 12:32:38 -0400 Subject: [PATCH 11/41] Add a glob test for optional packages --- cabal-install/parser-tests/Tests/ParserTests.hs | 7 +++++++ .../Tests/files/optional-packages/cabal.glob.project | 3 +++ 2 files changed, 10 insertions(+) create mode 100644 cabal-install/parser-tests/Tests/files/optional-packages/cabal.glob.project diff --git a/cabal-install/parser-tests/Tests/ParserTests.hs b/cabal-install/parser-tests/Tests/ParserTests.hs index 6ff00ea189a..919dc318989 100644 --- a/cabal-install/parser-tests/Tests/ParserTests.hs +++ b/cabal-install/parser-tests/Tests/ParserTests.hs @@ -76,6 +76,7 @@ parserTests = , 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 @@ -123,6 +124,12 @@ testOptionalPackages = do (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" 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}/ From fd302d9c4f545db2897eb9ecb5e599750fa33fc7 Mon Sep 17 00:00:00 2001 From: Phil de Joux Date: Tue, 28 Jul 2026 12:35:34 -0400 Subject: [PATCH 12/41] Add a glob parser for package location --- .../Client/ProjectConfig/FieldGrammar.hs | 53 ++++++++++++++++++- 1 file changed, 51 insertions(+), 2 deletions(-) diff --git a/cabal-install/src/Distribution/Client/ProjectConfig/FieldGrammar.hs b/cabal-install/src/Distribution/Client/ProjectConfig/FieldGrammar.hs index 9ba089bca17..d4be973e3e9 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.Newtype (Newtype) 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 <- monoidalFieldAla "packages" (alaList' FSep PackageLocationToken) L.projectPackages + projectPackagesOptional <- monoidalFieldAla "optional-packages" (alaList' FSep PackageLocationToken) L.projectPackagesOptional let projectPackagesRepo = mempty projectPackagesNamed <- monoidalFieldAla "extra-packages" formatPackageVersionConstraints L.projectPackagesNamed projectConfigBuildOnly <- blurFieldGrammar L.projectConfigBuildOnly projectConfigBuildOnlyFieldGrammar @@ -38,6 +43,50 @@ projectConfigFieldGrammar source knownPrograms = do projectConfigLocalPackages <- blurFieldGrammar L.projectConfigLocalPackages (packageConfigFieldGrammar knownPrograms) pure ProjectConfig{..} +newtype PackageLocationToken = PackageLocationToken {getPackageLocationToken :: String} + +instance Newtype String PackageLocationToken + +instance Parsec PackageLocationToken where + parsec = PackageLocationToken <$> parsePackageLocationTokenQ + +instance Pretty PackageLocationToken where + pretty = PP.text . renderPackageLocationToken . getPackageLocationToken + +-- | 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 From f1c43e6c75f346f9957e8d7383be1cd5ccb973c5 Mon Sep 17 00:00:00 2001 From: Phil de Joux Date: Tue, 28 Jul 2026 12:43:23 -0400 Subject: [PATCH 13/41] Avoid the use of Newtype --- .../Client/ProjectConfig/FieldGrammar.hs | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/cabal-install/src/Distribution/Client/ProjectConfig/FieldGrammar.hs b/cabal-install/src/Distribution/Client/ProjectConfig/FieldGrammar.hs index d4be973e3e9..96e2f78d444 100644 --- a/cabal-install/src/Distribution/Client/ProjectConfig/FieldGrammar.hs +++ b/cabal-install/src/Distribution/Client/ProjectConfig/FieldGrammar.hs @@ -16,7 +16,7 @@ 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.Newtype (Newtype) +import Distribution.Compat.Lens (Lens') import Distribution.Compat.Prelude import Distribution.FieldGrammar import Distribution.Parsec (CabalParsing, Parsec (..), parsecHaskellString) @@ -31,8 +31,8 @@ import qualified Text.PrettyPrint as PP projectConfigFieldGrammar :: ProjectConfigPath -> [String] -> ParsecFieldGrammar' ProjectConfig projectConfigFieldGrammar source knownPrograms = do - projectPackages <- monoidalFieldAla "packages" (alaList' FSep PackageLocationToken) L.projectPackages - projectPackagesOptional <- monoidalFieldAla "optional-packages" (alaList' FSep PackageLocationToken) 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 @@ -43,15 +43,22 @@ projectConfigFieldGrammar source knownPrograms = do projectConfigLocalPackages <- blurFieldGrammar L.projectConfigLocalPackages (packageConfigFieldGrammar knownPrograms) pure ProjectConfig{..} -newtype PackageLocationToken = PackageLocationToken {getPackageLocationToken :: String} +newtype PackageLocationTokens = PackageLocationTokens {getPackageLocationTokens :: [String]} -instance Newtype String PackageLocationToken +instance Semigroup PackageLocationTokens where + PackageLocationTokens a <> PackageLocationTokens b = PackageLocationTokens (a <> b) -instance Parsec PackageLocationToken where - parsec = PackageLocationToken <$> parsePackageLocationTokenQ +instance Monoid PackageLocationTokens where + mempty = PackageLocationTokens mempty -instance Pretty PackageLocationToken where - pretty = PP.text . renderPackageLocationToken . getPackageLocationToken +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 From 8321eb9d2930c0164bbbc602de96cab1f41ed6b8 Mon Sep 17 00:00:00 2001 From: Phil de Joux Date: Thu, 30 Jul 2026 10:56:25 -0400 Subject: [PATCH 14/41] For roundtripping, show delimited lines too --- .../tests/UnitTests/Distribution/Client/ProjectConfig.hs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/cabal-install/tests/UnitTests/Distribution/Client/ProjectConfig.hs b/cabal-install/tests/UnitTests/Distribution/Client/ProjectConfig.hs index 605d709b87d..f16f40f93d5 100644 --- a/cabal-install/tests/UnitTests/Distribution/Client/ProjectConfig.hs +++ b/cabal-install/tests/UnitTests/Distribution/Client/ProjectConfig.hs @@ -258,17 +258,19 @@ prop_roundtrip_legacytypes_specific config = -- roundtrip_printparse :: ProjectConfig -> Property -roundtrip_printparse config = +roundtrip_printparse config = countering $ case runParseResult $ parseProjectConfig "unused" (toUTF8BS str) of (_, Right result) -> - counterexample ("shown:\n" ++ str) $ ediffEq result{projectConfigProvenance = mempty} config{projectConfigProvenance = mempty} - (_, Left err) -> counterexample ("shown:\n" ++ str ++ "\nERROR: " ++ show err) False + (_, Left err) -> counterexample ("ERROR: " ++ show err) False where 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 = From 0702a8e78639dea446581a9149ebe42d448ea950 Mon Sep 17 00:00:00 2001 From: Phil de Joux Date: Thu, 30 Jul 2026 10:56:53 -0400 Subject: [PATCH 15/41] Add doctests for project parsers --- .../Client/ProjectConfig/Legacy.hs | 34 +++++++++++++++--- .../Client/ProjectConfig/Parsec.hs | 36 +++++++++++++++++++ 2 files changed, 65 insertions(+), 5 deletions(-) diff --git a/cabal-install/src/Distribution/Client/ProjectConfig/Legacy.hs b/cabal-install/src/Distribution/Client/ProjectConfig/Legacy.hs index fc4371638c0..297d73f8ce5 100644 --- a/cabal-install/src/Distribution/Client/ProjectConfig/Legacy.hs +++ b/cabal-install/src/Distribution/Client/ProjectConfig/Legacy.hs @@ -1273,14 +1273,29 @@ showLegacyProjectConfig config = constraintSrc = ConstraintSourceProjectConfig nullProjectConfigPath -- | --- >>> parseLegacyPackages "foo" +-- >>> parseLegacyConvert projectPackages "packages" "foo" -- ParseOk [] ["foo"] -- --- >>> parseLegacyPackages "xL{4,IE-,eK<}fE?e" +-- >>> parseLegacyConvert projectPackages "packages" "xL{4,IE-,eK<}fE?e" -- ParseOk [] ["xL{4,IE-,eK<}fE?e"] -- --- >>> parseLegacyPackages "7{u,{h,{=n}}}" +-- >>> parseLegacyConvert projectPackages "packages" "7{u,{h,{=n}}}" -- ParseOk [] ["7{u,{h,{=n}}}"] +-- +-- >>> parseLegacyConvert (packageConfigTestHumanLog . projectConfigAllPackages) "test-log" "" +-- ParseOk [] (Last {getLast = Nothing}) +-- +-- >>> parseLegacyConvert (packageConfigTestHumanLog . projectConfigAllPackages) "test-log" " " +-- ParseOk [] (Last {getLast = Nothing}) +-- +-- >>> parseLegacyConvert (packageConfigTestHumanLog . projectConfigAllPackages) "test-log" " \n" +-- ParseOk [] (Last {getLast = Nothing}) +-- +-- >>> parseLegacyConvert (packageConfigHaddockHtmlLocation . projectConfigAllPackages) "haddock-html-location" "" +-- ParseOk [] (Last {getLast = Nothing}) +-- +-- >>> parseLegacyConvert (packageConfigHaddockHtmlLocation . projectConfigAllPackages) "haddock-html-location" " " +-- ParseOk [] (Last {getLast = Nothing}) legacyProjectConfigFieldDescrs :: ConstraintSource -> [FieldDescr LegacyProjectConfig] legacyProjectConfigFieldDescrs constraintSrc = [ newLineListField @@ -2073,8 +2088,17 @@ showTokenQ x = showToken x -- $setup -- >>> :{ --- parseLegacyPackages s = legacyPackages <$> +-- parseLegacy :: (LegacyProjectConfig -> a) -> String -> String -> ParseResult a +-- parseLegacy f field s = f <$> +-- parseLegacyProjectConfigFieldsWithConstraintSource +-- ConstraintSourceUnknown +-- [ParseUtils.F 1 field s] +-- :} +-- +-- >>> :{ +-- parseLegacyConvert :: (ProjectConfig -> a) -> String -> String -> ParseResult a +-- parseLegacyConvert f field s = (f . convertLegacyProjectConfig) <$> -- parseLegacyProjectConfigFieldsWithConstraintSource -- ConstraintSourceUnknown --- [ParseUtils.F 1 "packages" s] +-- [ParseUtils.F 1 field s] -- :} diff --git a/cabal-install/src/Distribution/Client/ProjectConfig/Parsec.hs b/cabal-install/src/Distribution/Client/ProjectConfig/Parsec.hs index c7477837a62..179272d1fef 100644 --- a/cabal-install/src/Distribution/Client/ProjectConfig/Parsec.hs +++ b/cabal-install/src/Distribution/Client/ProjectConfig/Parsec.hs @@ -203,6 +203,30 @@ fieldsToConfig sourceConfigPath xs = do where programDb = defaultProgramDb +-- | +-- >>> 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 "test-log" "" +-- ([],Right []) +-- +-- >>> parseParsec (packageConfigTestHumanLog . projectConfigAllPackages) "test-log" " " +-- ([],Right (Last {getLast = Nothing})) +-- +-- >>> parseParsec (packageConfigTestHumanLog . projectConfigAllPackages) "test-log" " \n" +-- ([],Right (Last {getLast = Nothing})) + +-- >>> parseParsec (packageConfigHaddockHtmlLocation . projectConfigAllPackages) "haddock-html-location" "" +-- ([],Right (Last {getLast = Nothing})) + +-- >>> parseParsec (packageConfigHaddockHtmlLocation . projectConfigAllPackages) "haddock-html-location" " " +-- ([],Right (Last {getLast = Nothing})) parseProjectConfig :: FilePath -> BS.ByteString -> ParseResult ProjectFileSource ProjectConfig parseProjectConfig rootConfig bs = fieldsToConfig (ProjectConfigPath $ rootConfig :| []) =<< readPreprocessFields bs @@ -415,3 +439,15 @@ warnUnknownFields fieldName fieldLines = for_ fieldLines (\field -> parseWarning cabalSpec :: CabalSpecVersion cabalSpec = cabalSpecLatest + +-- $setup +-- >>> :set -XViewPatterns +-- >>> instance (Show a, Show b) => Show (ParseResult a b) where show = show . runParseResult +-- +-- >>> :{ +-- parseParsec :: (ProjectConfig -> a) -> String -> String -> ParseResult ProjectFileSource a +-- parseParsec f (toUTF8BS -> field) (toUTF8BS -> s) = f <$> +-- fieldsToConfig +-- nullProjectConfigPath +-- [Field (Name zeroPos field) [FieldLine zeroPos s]] +-- :} From f9e157a02ac645dff9bea7830bd7688c181db8b7 Mon Sep 17 00:00:00 2001 From: Phil de Joux Date: Thu, 30 Jul 2026 10:57:19 -0400 Subject: [PATCH 16/41] Add empty string local parser test --- cabal-install/parser-tests/Tests/ParserTests.hs | 9 +++++++++ .../cabal.empty-string.project | 1 + 2 files changed, 10 insertions(+) create mode 100644 cabal-install/parser-tests/Tests/files/project-config-local-packages/cabal.empty-string.project diff --git a/cabal-install/parser-tests/Tests/ParserTests.hs b/cabal-install/parser-tests/Tests/ParserTests.hs index 919dc318989..180dab7d583 100644 --- a/cabal-install/parser-tests/Tests/ParserTests.hs +++ b/cabal-install/parser-tests/Tests/ParserTests.hs @@ -86,6 +86,7 @@ parserTests = , testCase "read local-no-index-repos" testLocalNoIndexRepos , 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 @@ -419,6 +420,14 @@ testProjectConfigLocalPackages = do packageConfigTestTestOptions = [toPathTemplate "--some-option", toPathTemplate "42"] packageConfigBenchmarkOptions = [toPathTemplate "--some-benchmark-option", toPathTemplate "--another-option"] +testProjectConfigLocalPackagesEmptyString :: Assertion +testProjectConfigLocalPackagesEmptyString = do + (config, legacy) <- readConfig "project-config-local-packages" "cabal.empty-string.project" + assertConfigEquals expected config legacy (field . projectConfigLocalPackages . snd . condTreeData) + where + field = packageConfigTestHumanLog + expected = NoFlag + testProjectConfigAllPackages :: Assertion testProjectConfigAllPackages = do (config, legacy) <- readConfigDefault "project-config-all-packages" 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..3a9ccdbc3b6 --- /dev/null +++ b/cabal-install/parser-tests/Tests/files/project-config-local-packages/cabal.empty-string.project @@ -0,0 +1 @@ +test-log: From 87004aaf81272756b57064bf675f4dff41eb6f7c Mon Sep 17 00:00:00 2001 From: Phil de Joux Date: Wed, 23 Sep 2026 13:29:54 -0400 Subject: [PATCH 17/41] Satisfy fix-whitespace --- .../project-config-local-packages/cabal.empty-string.project | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 3a9ccdbc3b6..94079d00450 100644 --- 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 @@ -1 +1 @@ -test-log: +test-log: From 4a64a3ddd18e420aea84c57164f3d37218f67955 Mon Sep 17 00:00:00 2001 From: Phil de Joux Date: Wed, 23 Sep 2026 13:31:45 -0400 Subject: [PATCH 18/41] Satisfy fourmolu --- Cabal/src/Distribution/Simple/Compiler.hs | 8 ++++---- cabal-install/parser-tests/Tests/ParserTests.hs | 4 ++-- .../UnitTests/Distribution/Client/ProjectConfig.hs | 10 +++++----- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Cabal/src/Distribution/Simple/Compiler.hs b/Cabal/src/Distribution/Simple/Compiler.hs index 7ee10d85728..b33403958bb 100644 --- a/Cabal/src/Distribution/Simple/Compiler.hs +++ b/Cabal/src/Distribution/Simple/Compiler.hs @@ -358,11 +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@. + = -- | 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@. + | -- | The enabled level when enabled by @--enable-debug-info@ or @debug-info: True@. + NormalDebugInfo | MaximalDebugInfo deriving (Bounded, Enum, Eq, Generic, Read, Show) diff --git a/cabal-install/parser-tests/Tests/ParserTests.hs b/cabal-install/parser-tests/Tests/ParserTests.hs index 180dab7d583..805c5d7e3e2 100644 --- a/cabal-install/parser-tests/Tests/ParserTests.hs +++ b/cabal-install/parser-tests/Tests/ParserTests.hs @@ -102,7 +102,7 @@ parserTests = testPackages :: Assertion testPackages = do - let expected = [".", "packages/packages.cabal","a","b"] + let expected = [".", "packages/packages.cabal", "a", "b"] (config, legacy) <- readConfigDefault "packages" assertConfigEquals expected config legacy (projectPackages . snd . condTreeData) @@ -115,7 +115,7 @@ testPackagesGlob = do testPackagesCommaSeparated :: Assertion testPackagesCommaSeparated = do let expected = ["xL{4,IE-,eK<}fE?e"] - --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) diff --git a/cabal-install/tests/UnitTests/Distribution/Client/ProjectConfig.hs b/cabal-install/tests/UnitTests/Distribution/Client/ProjectConfig.hs index f16f40f93d5..b1630aaeeb3 100644 --- a/cabal-install/tests/UnitTests/Distribution/Client/ProjectConfig.hs +++ b/cabal-install/tests/UnitTests/Distribution/Client/ProjectConfig.hs @@ -261,16 +261,16 @@ roundtrip_printparse :: ProjectConfig -> Property roundtrip_printparse config = countering $ case runParseResult $ parseProjectConfig "unused" (toUTF8BS str) of (_, Right result) -> - ediffEq - result{projectConfigProvenance = mempty} - config{projectConfigProvenance = mempty} + ediffEq + result{projectConfigProvenance = mempty} + config{projectConfigProvenance = mempty} (_, Left err) -> counterexample ("ERROR: " ++ show err) False where str :: String str = showLegacyProjectConfig (convertToLegacyProjectConfig config) countering = - counterexample ("shown:\n" ++ str) . - counterexample ("shown by line:\n" ++ unlines (map (\s -> "'" ++ s ++ "'") (lines str))) + 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 = From 9c5e7b3fbe3a0fe0535d916e03f3a9848b223385 Mon Sep 17 00:00:00 2001 From: Phil de Joux Date: Wed, 23 Sep 2026 13:55:40 -0400 Subject: [PATCH 19/41] Use alternative for removed unpack' --- Cabal-syntax/src/Distribution/FieldGrammar/Newtypes.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cabal-syntax/src/Distribution/FieldGrammar/Newtypes.hs b/Cabal-syntax/src/Distribution/FieldGrammar/Newtypes.hs index 0e411649302..dce9abbafda 100644 --- a/Cabal-syntax/src/Distribution/FieldGrammar/Newtypes.hs +++ b/Cabal-syntax/src/Distribution/FieldGrammar/Newtypes.hs @@ -139,10 +139,10 @@ newtype List sep b a = List {_getList :: [a]} -- >>> :t alaList' FSep Token -- alaList' FSep Token :: [String] -> List FSep Token String -- --- >>> unpack' (alaList' FSep Token) <$> eitherParsec "foo bar foo" +-- >>> _getList <$> (eitherParsec "foo bar foo" :: Either String (List FSep Token String)) -- Right ["foo","bar","foo"] -- --- >>> unpack' (alaList' FSep Token) <$> eitherParsec "xL{4,IE-,eK<}fE?e" +-- >>> _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 From 3d1713cbf9b149d908f2920589bee59242a0b576 Mon Sep 17 00:00:00 2001 From: Phil de Joux Date: Wed, 23 Sep 2026 14:23:18 -0400 Subject: [PATCH 20/41] Grammar disallows parser setting in project --- .../tests/UnitTests/Distribution/Client/ProjectConfig.hs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cabal-install/tests/UnitTests/Distribution/Client/ProjectConfig.hs b/cabal-install/tests/UnitTests/Distribution/Client/ProjectConfig.hs index b1630aaeeb3..198cfde3635 100644 --- a/cabal-install/tests/UnitTests/Distribution/Client/ProjectConfig.hs +++ b/cabal-install/tests/UnitTests/Distribution/Client/ProjectConfig.hs @@ -592,7 +592,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 From 740cf31342f39fa5b366ed52df757c41a7fcd199 Mon Sep 17 00:00:00 2001 From: Phil de Joux Date: Wed, 23 Sep 2026 14:29:16 -0400 Subject: [PATCH 21/41] Expect negative for max-backjumps --- .../src/Distribution/Client/Utils/Newtypes.hs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) 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} From 9ab3e3405013de70ecbee0832be3b831d62b1fe4 Mon Sep 17 00:00:00 2001 From: Phil de Joux Date: Wed, 23 Sep 2026 14:40:15 -0400 Subject: [PATCH 22/41] Avoid empty and whitespace tokens - Legacy parser gave Flag "" - Parsec parser gives NoFlag --- .../Distribution/Client/ArbitraryInstances.hs | 15 ++++++++++----- .../Distribution/Client/ProjectConfig.hs | 14 +++++++------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/cabal-install/tests/UnitTests/Distribution/Client/ArbitraryInstances.hs b/cabal-install/tests/UnitTests/Distribution/Client/ArbitraryInstances.hs index 50f41e3eff1..fea9e5694e4 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 198cfde3635..bb70f8ca36c 100644 --- a/cabal-install/tests/UnitTests/Distribution/Client/ProjectConfig.hs +++ b/cabal-install/tests/UnitTests/Distribution/Client/ProjectConfig.hs @@ -637,15 +637,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 @@ -653,7 +653,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 From 6e6e52027a0febc0d2fa031d78b869ec049bb96c Mon Sep 17 00:00:00 2001 From: Phil de Joux Date: Wed, 23 Sep 2026 15:04:38 -0400 Subject: [PATCH 23/41] Don't generate commas for single-value fields --- Cabal-QuickCheck/src/Test/QuickCheck/Instances/Cabal.hs | 4 +++- .../tests/UnitTests/Distribution/Client/ArbitraryInstances.hs | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) 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-install/tests/UnitTests/Distribution/Client/ArbitraryInstances.hs b/cabal-install/tests/UnitTests/Distribution/Client/ArbitraryInstances.hs index fea9e5694e4..82107f37642 100644 --- a/cabal-install/tests/UnitTests/Distribution/Client/ArbitraryInstances.hs +++ b/cabal-install/tests/UnitTests/Distribution/Client/ArbitraryInstances.hs @@ -160,7 +160,7 @@ instance Arbitrary ShortToken where -- | The strings that the 'ShortToken' generator produces. isShortToken :: String -> Bool isShortToken cs = - all (\c -> c >= '#' && c <= '~' && c `notElem` "{}") cs + all (\c -> c >= '#' && c <= '~' && c `notElem` "{},") cs && not ("[]" `isPrefixOf` cs) arbitraryShortToken :: Gen String From f339a2e189e7a0e97909fb88694742a8b2122c50 Mon Sep 17 00:00:00 2001 From: Phil de Joux Date: Wed, 23 Sep 2026 16:03:32 -0400 Subject: [PATCH 24/41] Document & test that legacy sets empty fields to "" --- .../parser-tests/Tests/ParserTests.hs | 10 +++-- .../Client/ProjectConfig/Legacy.hs | 40 +++++++++---------- .../Client/ProjectConfig/Parsec.hs | 28 +++++++------ 3 files changed, 42 insertions(+), 36 deletions(-) diff --git a/cabal-install/parser-tests/Tests/ParserTests.hs b/cabal-install/parser-tests/Tests/ParserTests.hs index 805c5d7e3e2..e30cc085034 100644 --- a/cabal-install/parser-tests/Tests/ParserTests.hs +++ b/cabal-install/parser-tests/Tests/ParserTests.hs @@ -420,13 +420,17 @@ 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) <- readConfig "project-config-local-packages" "cabal.empty-string.project" - assertConfigEquals expected config legacy (field . projectConfigLocalPackages . snd . condTreeData) + assertEqual "Legacy parser sets the empty string" (toFlag (toPathTemplate "")) (field legacy) + assertEqual "Parsec parser leaves the field unset" NoFlag (field config) where - field = packageConfigTestHumanLog - expected = NoFlag + field = packageConfigTestHumanLog . projectConfigLocalPackages . snd . condTreeData testProjectConfigAllPackages :: Assertion testProjectConfigAllPackages = do diff --git a/cabal-install/src/Distribution/Client/ProjectConfig/Legacy.hs b/cabal-install/src/Distribution/Client/ProjectConfig/Legacy.hs index 297d73f8ce5..e0a715de3ee 100644 --- a/cabal-install/src/Distribution/Client/ProjectConfig/Legacy.hs +++ b/cabal-install/src/Distribution/Client/ProjectConfig/Legacy.hs @@ -1282,20 +1282,25 @@ showLegacyProjectConfig config = -- >>> parseLegacyConvert projectPackages "packages" "7{u,{h,{=n}}}" -- ParseOk [] ["7{u,{h,{=n}}}"] -- --- >>> parseLegacyConvert (packageConfigTestHumanLog . projectConfigAllPackages) "test-log" "" --- ParseOk [] (Last {getLast = Nothing}) +-- A top-level package field lands in the local packages, not in all packages. -- --- >>> parseLegacyConvert (packageConfigTestHumanLog . projectConfigAllPackages) "test-log" " " +-- >>> parseLegacyConvert (packageConfigTestHumanLog . projectConfigAllPackages) "test-log" "foo" -- ParseOk [] (Last {getLast = Nothing}) -- --- >>> parseLegacyConvert (packageConfigTestHumanLog . projectConfigAllPackages) "test-log" " \n" --- ParseOk [] (Last {getLast = Nothing}) +-- >>> parseLegacyConvert (packageConfigTestHumanLog . projectConfigLocalPackages) "test-log" "foo" +-- ParseOk [] (Last {getLast = Just "foo"}) -- --- >>> parseLegacyConvert (packageConfigHaddockHtmlLocation . projectConfigAllPackages) "haddock-html-location" "" --- ParseOk [] (Last {getLast = Nothing}) +-- 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 (packageConfigHaddockHtmlLocation . projectConfigAllPackages) "haddock-html-location" " " --- ParseOk [] (Last {getLast = Nothing}) +-- >>> 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 @@ -2087,18 +2092,13 @@ showTokenQ x@['.'] = Disp.text (show x) showTokenQ x = showToken x -- $setup --- >>> :{ --- parseLegacy :: (LegacyProjectConfig -> a) -> String -> String -> ParseResult a --- parseLegacy f field s = f <$> --- parseLegacyProjectConfigFieldsWithConstraintSource --- ConstraintSourceUnknown --- [ParseUtils.F 1 field s] --- :} +-- >>> 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) <$> --- parseLegacyProjectConfigFieldsWithConstraintSource --- ConstraintSourceUnknown --- [ParseUtils.F 1 field s] +-- 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 179272d1fef..c1adcb9aa14 100644 --- a/cabal-install/src/Distribution/Client/ProjectConfig/Parsec.hs +++ b/cabal-install/src/Distribution/Client/ProjectConfig/Parsec.hs @@ -213,19 +213,22 @@ fieldsToConfig sourceConfigPath xs = do -- >>> parseParsec projectPackages "packages" "7{u,{h,{=n}}}" -- ([],Right ["7{u,{h,{=n}}}"]) -- --- >>> parseParsec projectPackages "test-log" "" +-- >>> parseParsec projectPackages "packages" "" -- ([],Right []) -- --- >>> parseParsec (packageConfigTestHumanLog . projectConfigAllPackages) "test-log" " " --- ([],Right (Last {getLast = Nothing})) +-- >>> 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 . projectConfigAllPackages) "test-log" " \n" +-- >>> parseParsec (packageConfigTestHumanLog . projectConfigLocalPackages) "test-log" "" -- ([],Right (Last {getLast = Nothing})) - --- >>> parseParsec (packageConfigHaddockHtmlLocation . projectConfigAllPackages) "haddock-html-location" "" +-- +-- >>> parseParsec (packageConfigTestHumanLog . projectConfigLocalPackages) "test-log" " " -- ([],Right (Last {getLast = Nothing})) - --- >>> parseParsec (packageConfigHaddockHtmlLocation . projectConfigAllPackages) "haddock-html-location" " " +-- +-- >>> parseParsec (packageConfigHaddockHtmlLocation . projectConfigLocalPackages) "haddock-html-location" "" -- ([],Right (Last {getLast = Nothing})) parseProjectConfig :: FilePath -> BS.ByteString -> ParseResult ProjectFileSource ProjectConfig parseProjectConfig rootConfig bs = @@ -441,13 +444,12 @@ cabalSpec :: CabalSpecVersion cabalSpec = cabalSpecLatest -- $setup --- >>> :set -XViewPatterns -- >>> instance (Show a, Show b) => Show (ParseResult a b) where show = show . runParseResult -- +-- 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 (toUTF8BS -> field) (toUTF8BS -> s) = f <$> --- fieldsToConfig --- nullProjectConfigPath --- [Field (Name zeroPos field) [FieldLine zeroPos s]] +-- parseParsec f field s = f <$> parseProjectConfig "" (toUTF8BS (field ++ ": " ++ s)) -- :} From b0c00dea4d4f3bac8b4a7db84f5aea566f00fde0 Mon Sep 17 00:00:00 2001 From: Phil de Joux Date: Wed, 23 Sep 2026 16:08:45 -0400 Subject: [PATCH 25/41] Get rid of top level function only used by doctest --- .../src/Distribution/Client/ProjectConfig/Legacy.hs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/cabal-install/src/Distribution/Client/ProjectConfig/Legacy.hs b/cabal-install/src/Distribution/Client/ProjectConfig/Legacy.hs index e0a715de3ee..b6ca0b6c01c 100644 --- a/cabal-install/src/Distribution/Client/ProjectConfig/Legacy.hs +++ b/cabal-install/src/Distribution/Client/ProjectConfig/Legacy.hs @@ -1240,13 +1240,6 @@ convertToLegacyPerPackageConfig PackageConfig{..} = parseLegacyProjectConfigFields :: ProjectConfigPath -> [ParseUtils.Field] -> ParseResult LegacyProjectConfig parseLegacyProjectConfigFields (ConstraintSourceProjectConfig -> constraintSrc) = - parseLegacyProjectConfigFieldsWithConstraintSource constraintSrc - -parseLegacyProjectConfigFieldsWithConstraintSource - :: ConstraintSource - -> [ParseUtils.Field] - -> ParseResult LegacyProjectConfig -parseLegacyProjectConfigFieldsWithConstraintSource constraintSrc = parseFieldsAndSections (legacyProjectConfigFieldDescrs constraintSrc) legacyPackageConfigSectionDescrs From dd421ea7b6bcb44b6ff1401fe1fcf3d4df719f55 Mon Sep 17 00:00:00 2001 From: Phil de Joux Date: Wed, 23 Sep 2026 16:15:58 -0400 Subject: [PATCH 26/41] Add a changelog entry --- changelog.d/pr-12139.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 changelog.d/pr-12139.md diff --git a/changelog.d/pr-12139.md b/changelog.d/pr-12139.md new file mode 100644 index 00000000000..29986ccd87d --- /dev/null +++ b/changelog.d/pr-12139.md @@ -0,0 +1,19 @@ +--- +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}/`. + +The round trip property tests for project configuration now print with the +legacy printer and parse with the parsec parser. From 823d16c1804879cd642e9478bad414dcbe572890 Mon Sep 17 00:00:00 2001 From: Phil de Joux Date: Thu, 24 Sep 2026 12:32:11 -0400 Subject: [PATCH 27/41] debug-info now passes --- .../PackageTests/ProjectConfig/DebugInfo/cabal.out | 3 +++ .../PackageTests/ProjectConfig/DebugInfo/cabal.test.hs | 5 ++--- 2 files changed, 5 insertions(+), 3 deletions(-) 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.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 () From 2e3a0f6c57b98b34e8cb792b38bb2a3f3bc061ae Mon Sep 17 00:00:00 2001 From: Phil de Joux Date: Thu, 24 Sep 2026 12:38:43 -0400 Subject: [PATCH 28/41] Use normalise when reading --- .../src/Distribution/Client/ProjectConfig/Parsec.hs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/cabal-install/src/Distribution/Client/ProjectConfig/Parsec.hs b/cabal-install/src/Distribution/Client/ProjectConfig/Parsec.hs index c1adcb9aa14..03968a601a8 100644 --- a/cabal-install/src/Distribution/Client/ProjectConfig/Parsec.hs +++ b/cabal-install/src/Distribution/Client/ProjectConfig/Parsec.hs @@ -40,7 +40,6 @@ import Distribution.Simple.Program.Types (programName) import Distribution.Simple.Setup import Distribution.Simple.Utils (debug, noticeDoc) import Distribution.Solver.Types.ProjectConfigPath -import Distribution.System (buildOS) import Distribution.Types.CondTree (CondBranch (..), CondTree (..)) import Distribution.Types.ConfVar (ConfVar (..)) import Distribution.Types.PackageName (PackageName) @@ -57,7 +56,7 @@ import Distribution.Client.Errors.Parser (ProjectFileSource (..)) import qualified Distribution.Compat.CharParsing as P import Network.URI (URI, uriFragment, uriPath, uriScheme) import System.Directory (makeAbsolute) -import System.FilePath (splitFileName) +import System.FilePath (normalise, splitFileName) import qualified Text.Parsec import Text.PrettyPrint (render) import qualified Text.PrettyPrint as Disp @@ -322,8 +321,8 @@ 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 + return $ Left $ LocalRepo (remoteRepoName repo) (normalise (uriPath uri)) (uriFragment uri == "#shared-cache") _ -> do when (remoteRepoKeyThreshold repo > length (remoteRepoRootKeys repo)) $ warning $ From 65108a9c64697dfa47128794a23cfb81069490a3 Mon Sep 17 00:00:00 2001 From: Phil de Joux Date: Thu, 24 Sep 2026 12:49:32 -0400 Subject: [PATCH 29/41] Add fileNoIndexURIPath, haddocks and doctests --- .../src/Distribution/Client/Config.hs | 7 +-- .../Client/ProjectConfig/Parsec.hs | 13 +++-- .../src/Distribution/Client/Types/Repo.hs | 50 +++++++++++++++++++ 3 files changed, 64 insertions(+), 6 deletions(-) diff --git a/cabal-install/src/Distribution/Client/Config.hs b/cabal-install/src/Distribution/Client/Config.hs index f62caaf35ad..680486c2aa2 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,7 @@ postProcessRepo lineno reponameStr repo0 = do Left $ LocalRepo reponame - (normalise (uriPath uri)) + (fileNoIndexURIPath buildOS uri) (uriFragment uri == "#shared-cache") _ -> do let repo = repo0{remoteRepoName = reponame} diff --git a/cabal-install/src/Distribution/Client/ProjectConfig/Parsec.hs b/cabal-install/src/Distribution/Client/ProjectConfig/Parsec.hs index 03968a601a8..7b51f18b805 100644 --- a/cabal-install/src/Distribution/Client/ProjectConfig/Parsec.hs +++ b/cabal-install/src/Distribution/Client/ProjectConfig/Parsec.hs @@ -40,6 +40,7 @@ import Distribution.Simple.Program.Types (programName) import Distribution.Simple.Setup import Distribution.Simple.Utils (debug, noticeDoc) import Distribution.Solver.Types.ProjectConfigPath +import Distribution.System (buildOS) import Distribution.Types.CondTree (CondBranch (..), CondTree (..)) import Distribution.Types.ConfVar (ConfVar (..)) import Distribution.Types.PackageName (PackageName) @@ -54,9 +55,9 @@ 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 (normalise, splitFileName) +import System.FilePath (splitFileName) import qualified Text.Parsec import Text.PrettyPrint (render) import qualified Text.PrettyPrint as Disp @@ -316,13 +317,19 @@ 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 = remoteRepoURI repo - return $ Left $ LocalRepo (remoteRepoName repo) (normalise (uriPath uri)) (uriFragment uri == "#shared-cache") + return $ Left $ LocalRepo (remoteRepoName repo) (fileNoIndexURIPath buildOS uri) (uriFragment uri == "#shared-cache") _ -> do when (remoteRepoKeyThreshold repo > length (remoteRepoRootKeys repo)) $ warning $ diff --git a/cabal-install/src/Distribution/Client/Types/Repo.hs b/cabal-install/src/Distribution/Client/Types/Repo.hs index f01bfa744bf..0cbb5ae7c6d 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,43 @@ 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. Both the legacy and parsec +-- project file parsers read the path this way. Reading it with +-- 'normaliseFileNoIndexURI' instead would keep the POSIX-style slashes on +-- Windows and the two parsers would disagree with each other and with the +-- path the URI was written from. +-- +-- 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 From 5cb58d6ef2c954eb1e061b2a1c3d2f18fe15c95c Mon Sep 17 00:00:00 2001 From: Phil de Joux Date: Thu, 24 Sep 2026 15:24:16 -0400 Subject: [PATCH 30/41] Expect native local repo paths in the parser test --- cabal-install/parser-tests/Tests/ParserTests.hs | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/cabal-install/parser-tests/Tests/ParserTests.hs b/cabal-install/parser-tests/Tests/ParserTests.hs index e30cc085034..d7e14b35e31 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,7 +61,7 @@ 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) @@ -321,26 +320,24 @@ 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 testProjectConfigProvenance :: Assertion testProjectConfigProvenance = do From 89c7b8c53aa6ae8ab9dc0d17f5e37416fcdf72fd Mon Sep 17 00:00:00 2001 From: Phil de Joux Date: Tue, 29 Sep 2026 08:31:16 -0400 Subject: [PATCH 31/41] Say why local repo paths are stored native --- cabal-install/src/Distribution/Client/Config.hs | 2 ++ .../Distribution/Client/ProjectConfig/Parsec.hs | 2 ++ .../src/Distribution/Client/Types/Repo.hs | 14 +++++++++----- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/cabal-install/src/Distribution/Client/Config.hs b/cabal-install/src/Distribution/Client/Config.hs index 680486c2aa2..9833c5952a2 100644 --- a/cabal-install/src/Distribution/Client/Config.hs +++ b/cabal-install/src/Distribution/Client/Config.hs @@ -1690,6 +1690,8 @@ postProcessRepo lineno reponameStr repo0 = do Left $ LocalRepo reponame + -- 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 diff --git a/cabal-install/src/Distribution/Client/ProjectConfig/Parsec.hs b/cabal-install/src/Distribution/Client/ProjectConfig/Parsec.hs index 7b51f18b805..0b08f08dc67 100644 --- a/cabal-install/src/Distribution/Client/ProjectConfig/Parsec.hs +++ b/cabal-install/src/Distribution/Client/ProjectConfig/Parsec.hs @@ -329,6 +329,8 @@ postProcessRemoteRepo pos repo = case uriScheme (remoteRepoURI repo) of -- Note: the trailing colon is important "file+noindex:" -> do 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)) $ diff --git a/cabal-install/src/Distribution/Client/Types/Repo.hs b/cabal-install/src/Distribution/Client/Types/Repo.hs index 0cbb5ae7c6d..08da108d7be 100644 --- a/cabal-install/src/Distribution/Client/Types/Repo.hs +++ b/cabal-install/src/Distribution/Client/Types/Repo.hs @@ -272,11 +272,15 @@ normaliseFileNoIndexURI os uri@(URI scheme _auth path query fragment) -- 'localRepoPath' of a 'LocalRepo'. -- -- This is the reading direction, the inverse of 'normaliseFileNoIndexURI'. --- Use it when parsing a @repository@ section. Both the legacy and parsec --- project file parsers read the path this way. Reading it with --- 'normaliseFileNoIndexURI' instead would keep the POSIX-style slashes on --- Windows and the two parsers would disagree with each other and with the --- path the URI was written from. +-- 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 it is a +-- 'FilePath' like every other in cabal-install, because 'localRepoCacheKey' +-- hashes its spelling to name a cache directory, and because 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 and break all three. -- -- On Windows the path is normalised to backslashes, whether it was written -- POSIX-style by 'normaliseFileNoIndexURI' or with backslashes by hand. From 3c75feb897b0ac3ab561788896f822f9339217ac Mon Sep 17 00:00:00 2001 From: Phil de Joux Date: Tue, 29 Sep 2026 08:34:06 -0400 Subject: [PATCH 32/41] Spell out the uses of paths --- .../src/Distribution/Client/Types/Repo.hs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/cabal-install/src/Distribution/Client/Types/Repo.hs b/cabal-install/src/Distribution/Client/Types/Repo.hs index 08da108d7be..352936e17bb 100644 --- a/cabal-install/src/Distribution/Client/Types/Repo.hs +++ b/cabal-install/src/Distribution/Client/Types/Repo.hs @@ -275,12 +275,19 @@ normaliseFileNoIndexURI os uri@(URI scheme _auth path query fragment) -- 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 it is a --- 'FilePath' like every other in cabal-install, because 'localRepoCacheKey' --- hashes its spelling to name a cache directory, and because 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 and break all three. +-- 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. From a277019f84ea4406bd5d3b4f9b5e0f7c05672ccb Mon Sep 17 00:00:00 2001 From: Phil de Joux Date: Tue, 29 Sep 2026 08:56:55 -0400 Subject: [PATCH 33/41] Assert both parsers agree in readConfig - Add readConfigDiverging --- cabal-install/parser-tests/Tests/ParserTests.hs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/cabal-install/parser-tests/Tests/ParserTests.hs b/cabal-install/parser-tests/Tests/ParserTests.hs index d7e14b35e31..721943c6533 100644 --- a/cabal-install/parser-tests/Tests/ParserTests.hs +++ b/cabal-install/parser-tests/Tests/ParserTests.hs @@ -423,7 +423,7 @@ testProjectConfigLocalPackages = do -- leaves it unset, which is the better behaviour. testProjectConfigLocalPackagesEmptyString :: Assertion testProjectConfigLocalPackagesEmptyString = do - (config, legacy) <- readConfig "project-config-local-packages" "cabal.empty-string.project" + (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 @@ -595,8 +595,19 @@ 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 + assertEqual "Parsec parser disagrees with the legacy parser" legacy parsec + return (parsec, legacy) + +-- | 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 From 597d36742a924f23204ce338213493225cf54738 Mon Sep 17 00:00:00 2001 From: Phil de Joux Date: Tue, 29 Sep 2026 09:02:52 -0400 Subject: [PATCH 34/41] Show a tree diff when the parsers disagree --- cabal-install/cabal-install.cabal | 5 ++++- cabal-install/parser-tests/Tests/ParserTests.hs | 14 ++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) 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 721943c6533..2c63b7d59bd 100644 --- a/cabal-install/parser-tests/Tests/ParserTests.hs +++ b/cabal-install/parser-tests/Tests/ParserTests.hs @@ -65,7 +65,10 @@ 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 = @@ -601,9 +604,16 @@ readConfigDefault testSubDir = readConfig testSubDir "cabal.project" readConfig :: FilePath -> FilePath -> IO (ProjectConfigSkeleton, ProjectConfigSkeleton) readConfig testSubDir projectFileName = do (parsec, legacy) <- readConfigDiverging testSubDir projectFileName - assertEqual "Parsec parser disagrees with the legacy parser" legacy parsec + 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) From 94c31a1aa4846af5c00ba82c415896a119d51d79 Mon Sep 17 00:00:00 2001 From: Phil de Joux Date: Tue, 29 Sep 2026 09:08:20 -0400 Subject: [PATCH 35/41] Parse the round trip text with both parsers --- .../Distribution/Client/ProjectConfig.hs | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/cabal-install/tests/UnitTests/Distribution/Client/ProjectConfig.hs b/cabal-install/tests/UnitTests/Distribution/Client/ProjectConfig.hs index bb70f8ca36c..d3f8c03d413 100644 --- a/cabal-install/tests/UnitTests/Distribution/Client/ProjectConfig.hs +++ b/cabal-install/tests/UnitTests/Distribution/Client/ProjectConfig.hs @@ -17,6 +17,7 @@ import System.Directory (canonicalizePath, withCurrentDirectory) import System.FilePath import System.IO.Unsafe (unsafePerformIO) +import Distribution.Deprecated.ParseUtils (ParseResult (..)) import qualified Distribution.Deprecated.ReadP as Parse import Distribution.Package @@ -257,15 +258,27 @@ 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 = countering $ - case runParseResult $ parseProjectConfig "unused" (toUTF8BS str) of - (_, Right result) -> +roundtrip_printparse config = + 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} - config{projectConfigProvenance = mempty} - (_, Left err) -> counterexample ("ERROR: " ++ show err) False - where + expected{projectConfigProvenance = mempty} + bs = toUTF8BS str str :: String str = showLegacyProjectConfig (convertToLegacyProjectConfig config) countering = From c5d2b9bb56838e68fc1069a975d001806c618862 Mon Sep 17 00:00:00 2001 From: Phil de Joux Date: Tue, 29 Sep 2026 10:13:32 -0400 Subject: [PATCH 36/41] Allow max-backjumps=-1 in docs --- doc/cabal-project-description-file.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/cabal-project-description-file.rst b/doc/cabal-project-description-file.rst index 4b3858862f4..345081a3212 100644 --- a/doc/cabal-project-description-file.rst +++ b/doc/cabal-project-description-file.rst @@ -1999,7 +1999,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. From 7bbe2f5f0966463e0e7989ddf7972df627c58057 Mon Sep 17 00:00:00 2001 From: Phil de Joux Date: Tue, 29 Sep 2026 10:29:22 -0400 Subject: [PATCH 37/41] Add a test for parser selection as project field --- .../ProjectConfig/ProjectFileParser/cabal.out | 11 +++++++++++ .../ProjectConfig/ProjectFileParser/cabal.project | 2 ++ .../ProjectConfig/ProjectFileParser/cabal.test.hs | 14 ++++++++++++++ .../ProjectFileParser/project-file-parser.cabal | 7 +++++++ 4 files changed, 34 insertions(+) create mode 100644 cabal-testsuite/PackageTests/ProjectConfig/ProjectFileParser/cabal.out create mode 100644 cabal-testsuite/PackageTests/ProjectConfig/ProjectFileParser/cabal.project create mode 100644 cabal-testsuite/PackageTests/ProjectConfig/ProjectFileParser/cabal.test.hs create mode 100644 cabal-testsuite/PackageTests/ProjectConfig/ProjectFileParser/project-file-parser.cabal diff --git a/cabal-testsuite/PackageTests/ProjectConfig/ProjectFileParser/cabal.out b/cabal-testsuite/PackageTests/ProjectConfig/ProjectFileParser/cabal.out new file mode 100644 index 00000000000..e85b3acbdc9 --- /dev/null +++ b/cabal-testsuite/PackageTests/ProjectConfig/ProjectFileParser/cabal.out @@ -0,0 +1,11 @@ +# cabal build +Resolving dependencies... +Build profile: -w ghc- -O1 +In order, the following would be built: + - project-file-parser-0 (lib) (first run) +# cabal build +Warnings found while parsing the project file, cabal.project: + - cabal.project:2:1: Unknown field: "project-file-parser" +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..f78749f3a96 --- /dev/null +++ b/cabal-testsuite/PackageTests/ProjectConfig/ProjectFileParser/cabal.test.hs @@ -0,0 +1,14 @@ +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. The +-- legacy parser accepts it without a word and the parsec parser warns. +main = cabalTest . recordMode RecordMarked $ do + let warning = "Unknown field: \"project-file-parser\"" + + legacy <- cabal' "build" ["--dry-run", "--project-file-parser=legacy"] + assertOutputDoesNotContain warning legacy + + parsec <- cabal' "build" ["--dry-run", "--project-file-parser=parsec"] + assertOutputContains warning parsec + pure () 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 From dea9543d9ba756634a331da9500cdeb782dda8dd Mon Sep 17 00:00:00 2001 From: Phil de Joux Date: Tue, 29 Sep 2026 10:33:51 -0400 Subject: [PATCH 38/41] Separate the .out files --- .../ProjectFileParser/cabal.legacy.out | 5 +++++ .../{cabal.out => cabal.parsec.out} | 6 +----- .../ProjectConfig/ProjectFileParser/cabal.test.hs | 13 +++++++------ 3 files changed, 13 insertions(+), 11 deletions(-) create mode 100644 cabal-testsuite/PackageTests/ProjectConfig/ProjectFileParser/cabal.legacy.out rename cabal-testsuite/PackageTests/ProjectConfig/ProjectFileParser/{cabal.out => cabal.parsec.out} (67%) 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..92f15e1f054 --- /dev/null +++ b/cabal-testsuite/PackageTests/ProjectConfig/ProjectFileParser/cabal.legacy.out @@ -0,0 +1,5 @@ +# cabal build +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.out b/cabal-testsuite/PackageTests/ProjectConfig/ProjectFileParser/cabal.parsec.out similarity index 67% rename from cabal-testsuite/PackageTests/ProjectConfig/ProjectFileParser/cabal.out rename to cabal-testsuite/PackageTests/ProjectConfig/ProjectFileParser/cabal.parsec.out index e85b3acbdc9..40f9a4366f1 100644 --- a/cabal-testsuite/PackageTests/ProjectConfig/ProjectFileParser/cabal.out +++ b/cabal-testsuite/PackageTests/ProjectConfig/ProjectFileParser/cabal.parsec.out @@ -1,11 +1,7 @@ # cabal build -Resolving dependencies... -Build profile: -w ghc- -O1 -In order, the following would be built: - - project-file-parser-0 (lib) (first run) -# cabal build Warnings found while parsing the project file, cabal.project: - cabal.project:2:1: Unknown field: "project-file-parser" +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.test.hs b/cabal-testsuite/PackageTests/ProjectConfig/ProjectFileParser/cabal.test.hs index f78749f3a96..9fd9c664b2c 100644 --- a/cabal-testsuite/PackageTests/ProjectConfig/ProjectFileParser/cabal.test.hs +++ b/cabal-testsuite/PackageTests/ProjectConfig/ProjectFileParser/cabal.test.hs @@ -3,12 +3,13 @@ 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. The -- legacy parser accepts it without a word and the parsec parser warns. -main = cabalTest . recordMode RecordMarked $ do +main = do let warning = "Unknown field: \"project-file-parser\"" - legacy <- cabal' "build" ["--dry-run", "--project-file-parser=legacy"] - assertOutputDoesNotContain warning legacy + cabalTest' "legacy" . recordMode RecordMarked $ do + legacy <- cabal' "build" ["--dry-run", "--project-file-parser=legacy"] + assertOutputDoesNotContain warning legacy - parsec <- cabal' "build" ["--dry-run", "--project-file-parser=parsec"] - assertOutputContains warning parsec - pure () + cabalTest' "parsec" . recordMode RecordMarked $ do + parsec <- cabal' "build" ["--dry-run", "--project-file-parser=parsec"] + assertOutputContains warning parsec From 4e548ff327d10c8d7627b7d5f2f45b1888af13c8 Mon Sep 17 00:00:00 2001 From: Phil de Joux Date: Tue, 29 Sep 2026 10:45:06 -0400 Subject: [PATCH 39/41] Add a warning about project-file-parser as field --- .../parser-tests/Tests/ParserTests.hs | 8 +++++++ .../files/project-file-parser/cabal.project | 1 + .../Client/ProjectConfig/Legacy.hs | 3 +++ .../Client/ProjectConfig/Parsec.hs | 24 ++++++++++++++++++- .../ProjectFileParser/cabal.legacy.out | 2 ++ .../ProjectFileParser/cabal.parsec.out | 2 +- .../ProjectFileParser/cabal.test.hs | 8 +++---- changelog.d/pr-12139.md | 5 ++++ 8 files changed, 47 insertions(+), 6 deletions(-) create mode 100644 cabal-install/parser-tests/Tests/files/project-file-parser/cabal.project diff --git a/cabal-install/parser-tests/Tests/ParserTests.hs b/cabal-install/parser-tests/Tests/ParserTests.hs index 2c63b7d59bd..df2789c038d 100644 --- a/cabal-install/parser-tests/Tests/ParserTests.hs +++ b/cabal-install/parser-tests/Tests/ParserTests.hs @@ -86,6 +86,7 @@ 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 @@ -342,6 +343,13 @@ testLocalNoIndexRepos = do , localRepoSharedCache = False } +-- | 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 let expected = Set.singleton (Explicit (ProjectConfigPath $ "cabal.project" :| [])) 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/ProjectConfig/Legacy.hs b/cabal-install/src/Distribution/Client/ProjectConfig/Legacy.hs index b6ca0b6c01c..54c92beb0a7 100644 --- a/cabal-install/src/Distribution/Client/ProjectConfig/Legacy.hs +++ b/cabal-install/src/Distribution/Client/ProjectConfig/Legacy.hs @@ -1509,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)] diff --git a/cabal-install/src/Distribution/Client/ProjectConfig/Parsec.hs b/cabal-install/src/Distribution/Client/ProjectConfig/Parsec.hs index 0b08f08dc67..52587164321 100644 --- a/cabal-install/src/Distribution/Client/ProjectConfig/Parsec.hs +++ b/cabal-install/src/Distribution/Client/ProjectConfig/Parsec.hs @@ -197,12 +197,25 @@ fieldsToConfig :: ProjectConfigPath -> [Field Position] -> ParseResult ProjectFi fieldsToConfig sourceConfigPath xs = do let (fs, sectionGroups) = partitionFields xs sections = concat sectionGroups - config <- parseFieldGrammarCheckingStanzas cabalSpec fs (projectConfigFieldGrammar sourceConfigPath (knownProgramNames programDb)) stanzas + 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"]) @@ -230,6 +243,14 @@ fieldsToConfig sourceConfigPath xs = do -- -- >>> 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 @@ -453,6 +474,7 @@ 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. diff --git a/cabal-testsuite/PackageTests/ProjectConfig/ProjectFileParser/cabal.legacy.out b/cabal-testsuite/PackageTests/ProjectConfig/ProjectFileParser/cabal.legacy.out index 92f15e1f054..2a64d409b7e 100644 --- a/cabal-testsuite/PackageTests/ProjectConfig/ProjectFileParser/cabal.legacy.out +++ b/cabal-testsuite/PackageTests/ProjectConfig/ProjectFileParser/cabal.legacy.out @@ -1,4 +1,6 @@ # 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: diff --git a/cabal-testsuite/PackageTests/ProjectConfig/ProjectFileParser/cabal.parsec.out b/cabal-testsuite/PackageTests/ProjectConfig/ProjectFileParser/cabal.parsec.out index 40f9a4366f1..41f6933239d 100644 --- a/cabal-testsuite/PackageTests/ProjectConfig/ProjectFileParser/cabal.parsec.out +++ b/cabal-testsuite/PackageTests/ProjectConfig/ProjectFileParser/cabal.parsec.out @@ -1,6 +1,6 @@ # cabal build Warnings found while parsing the project file, cabal.project: - - cabal.project:2:1: Unknown field: "project-file-parser" + - 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: diff --git a/cabal-testsuite/PackageTests/ProjectConfig/ProjectFileParser/cabal.test.hs b/cabal-testsuite/PackageTests/ProjectConfig/ProjectFileParser/cabal.test.hs index 9fd9c664b2c..b0be8b3e62c 100644 --- a/cabal-testsuite/PackageTests/ProjectConfig/ProjectFileParser/cabal.test.hs +++ b/cabal-testsuite/PackageTests/ProjectConfig/ProjectFileParser/cabal.test.hs @@ -1,14 +1,14 @@ 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. The --- legacy parser accepts it without a word and the parsec parser warns. +-- 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 = "Unknown field: \"project-file-parser\"" + 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"] - assertOutputDoesNotContain warning legacy + assertOutputContains "Unrecognized field 'project-file-parser'" legacy cabalTest' "parsec" . recordMode RecordMarked $ do parsec <- cabal' "build" ["--dry-run", "--project-file-parser=parsec"] diff --git a/changelog.d/pr-12139.md b/changelog.d/pr-12139.md index 29986ccd87d..9235f0e87c1 100644 --- a/changelog.d/pr-12139.md +++ b/changelog.d/pr-12139.md @@ -15,5 +15,10 @@ legacy parser accepted before: - 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. From 531cbb1cd264690d6508a173f7059b3589d9dee4 Mon Sep 17 00:00:00 2001 From: Phil de Joux Date: Tue, 29 Sep 2026 10:54:29 -0400 Subject: [PATCH 40/41] Mention the warning in the docs --- doc/cabal-project-description-file.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/cabal-project-description-file.rst b/doc/cabal-project-description-file.rst index 345081a3212..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 From 1769cc936f7fbebc2020addd6fe4c577f123e9f6 Mon Sep 17 00:00:00 2001 From: Phil de Joux Date: Tue, 29 Sep 2026 10:59:20 -0400 Subject: [PATCH 41/41] Follow hlint suggestion: redundant $ --- cabal-install/src/Distribution/Client/ProjectConfig/Parsec.hs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cabal-install/src/Distribution/Client/ProjectConfig/Parsec.hs b/cabal-install/src/Distribution/Client/ProjectConfig/Parsec.hs index 52587164321..01f912bd909 100644 --- a/cabal-install/src/Distribution/Client/ProjectConfig/Parsec.hs +++ b/cabal-install/src/Distribution/Client/ProjectConfig/Parsec.hs @@ -213,7 +213,9 @@ projectFileParserField = "project-file-parser" warnProjectFileParserField :: Fields Position -> ParseResult src () warnProjectFileParserField fs = for_ (Map.findWithDefault [] projectFileParserField fs) $ \field -> - parseWarning (namelessFieldAnn field) PWTOther $ + 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." -- |