Skip to content

Commit 400a589

Browse files
authored
Merge pull request #9578 from cabalism/fix/solver-msg-import-tree-9562
Show import tree provenance
2 parents 661aeca + f8cd563 commit 400a589

File tree

36 files changed

+649
-484
lines changed

36 files changed

+649
-484
lines changed

cabal-install-solver/cabal-install-solver.cabal

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ library
9292
Distribution.Solver.Types.PackagePreferences
9393
Distribution.Solver.Types.PkgConfigDb
9494
Distribution.Solver.Types.Progress
95+
Distribution.Solver.Types.ProjectConfigPath
9596
Distribution.Solver.Types.ResolverPackage
9697
Distribution.Solver.Types.Settings
9798
Distribution.Solver.Types.SolverId
@@ -107,8 +108,10 @@ library
107108
, Cabal-syntax ^>=3.11
108109
, containers >=0.5.6.2 && <0.8
109110
, edit-distance ^>= 0.2.2
111+
, directory >= 1.3.7.0 && < 1.4
110112
, filepath ^>=1.4.0.0 || ^>=1.5.0.0
111113
, mtl >=2.0 && <2.4
114+
, network-uri >= 2.6.0.2 && < 2.7
112115
, pretty ^>=1.1
113116
, transformers >=0.4.2.0 && <0.7
114117
, text (>= 1.2.3.0 && < 1.3) || (>= 2.0 && < 2.2)

cabal-install-solver/src/Distribution/Solver/Modular/Message.hs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,10 @@ import Distribution.Solver.Modular.Version
3131
import Distribution.Solver.Types.ConstraintSource
3232
import Distribution.Solver.Types.PackagePath
3333
import Distribution.Solver.Types.Progress
34+
import Distribution.Solver.Types.ProjectConfigPath (docProjectConfigPathFailReason)
3435
import Distribution.Types.LibraryName
3536
import Distribution.Types.UnqualComponentName
37+
import Text.PrettyPrint (nest, render)
3638

3739
data Message =
3840
Enter -- ^ increase indentation level
@@ -311,6 +313,7 @@ showFR _ NotExplicit = " (not a user-provided goal nor ment
311313
showFR _ Shadowed = " (shadowed by another installed package with same version)"
312314
showFR _ (Broken u) = " (package is broken, missing dependency " ++ prettyShow u ++ ")"
313315
showFR _ UnknownPackage = " (unknown package)"
316+
showFR _ (GlobalConstraintVersion vr (ConstraintSourceProjectConfig pc)) = '\n' : (render . nest 6 $ docProjectConfigPathFailReason vr pc)
314317
showFR _ (GlobalConstraintVersion vr src) = " (" ++ constraintSource src ++ " requires " ++ prettyShow vr ++ ")"
315318
showFR _ (GlobalConstraintInstalled src) = " (" ++ constraintSource src ++ " requires installed instance)"
316319
showFR _ (GlobalConstraintSource src) = " (" ++ constraintSource src ++ " requires source instance)"

cabal-install-solver/src/Distribution/Solver/Types/ConstraintSource.hs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ module Distribution.Solver.Types.ConstraintSource
55
) where
66

77
import Distribution.Solver.Compat.Prelude
8-
import Prelude ()
8+
import Distribution.Solver.Types.ProjectConfigPath (ProjectConfigPath, docProjectConfigPath)
9+
import Text.PrettyPrint (render)
910

1011
-- | Source of a 'PackageConstraint'.
1112
data ConstraintSource =
@@ -14,7 +15,7 @@ data ConstraintSource =
1415
ConstraintSourceMainConfig FilePath
1516

1617
-- | Local cabal.project file
17-
| ConstraintSourceProjectConfig FilePath
18+
| ConstraintSourceProjectConfig ProjectConfigPath
1819

1920
-- | User config file, which is ./cabal.config by default.
2021
| ConstraintSourceUserConfig FilePath
@@ -60,7 +61,7 @@ showConstraintSource :: ConstraintSource -> String
6061
showConstraintSource (ConstraintSourceMainConfig path) =
6162
"main config " ++ path
6263
showConstraintSource (ConstraintSourceProjectConfig path) =
63-
"project config " ++ path
64+
"project config " ++ render (docProjectConfigPath path)
6465
showConstraintSource (ConstraintSourceUserConfig path)= "user config " ++ path
6566
showConstraintSource ConstraintSourceCommandlineFlag = "command line flag"
6667
showConstraintSource ConstraintSourceUserTarget = "user target"
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
{-# LANGUAGE DeriveGeneric #-}
2+
{-# LANGUAGE LambdaCase #-}
3+
4+
module Distribution.Solver.Types.ProjectConfigPath
5+
(
6+
-- * Project Config Path Manipulation
7+
ProjectConfigPath(..)
8+
, projectConfigPathRoot
9+
, nullProjectConfigPath
10+
, consProjectConfigPath
11+
12+
-- * Messages
13+
, docProjectConfigPath
14+
, cyclicalImportMsg
15+
, docProjectConfigPathFailReason
16+
17+
-- * Checks and Normalization
18+
, isCyclicConfigPath
19+
, canonicalizeConfigPath
20+
) where
21+
22+
import Distribution.Solver.Compat.Prelude hiding (toList, (<>))
23+
import Prelude (sequence)
24+
25+
import Data.Coerce (coerce)
26+
import Data.List.NonEmpty ((<|))
27+
import Network.URI (parseURI)
28+
import System.Directory
29+
import System.FilePath
30+
import qualified Data.List.NonEmpty as NE
31+
import Distribution.Solver.Modular.Version (VR)
32+
import Distribution.Pretty (prettyShow)
33+
import Text.PrettyPrint
34+
35+
-- | Path to a configuration file, either a singleton project root, or a longer
36+
-- list representing a path to an import. The path is a non-empty list that we
37+
-- build up by prepending relative imports with @consProjectConfigPath@.
38+
--
39+
-- An import can be a URI, such as [a stackage
40+
-- cabal.config](https://www.stackage.org/nightly/cabal.config), but we do not
41+
-- support URIs in the middle of the path, URIs that import other URIs, or URIs
42+
-- that import local files.
43+
--
44+
-- List elements are relative to each other but once canonicalized, elements are
45+
-- relative to the directory of the project root.
46+
newtype ProjectConfigPath = ProjectConfigPath (NonEmpty FilePath)
47+
deriving (Eq, Ord, Show, Generic)
48+
49+
instance Binary ProjectConfigPath
50+
instance Structured ProjectConfigPath
51+
52+
-- | Renders the path like this;
53+
-- @
54+
-- D.config
55+
-- imported by: C.config
56+
-- imported by: B.config
57+
-- imported by: A.project
58+
-- @
59+
-- >>> render . docProjectConfigPath $ ProjectConfigPath $ "D.config" :| ["C.config", "B.config", "A.project" ]
60+
-- "D.config\n imported by: C.config\n imported by: B.config\n imported by: A.project"
61+
docProjectConfigPath :: ProjectConfigPath -> Doc
62+
docProjectConfigPath (ProjectConfigPath (p :| [])) = text p
63+
docProjectConfigPath (ProjectConfigPath (p :| ps)) = vcat $
64+
text p : [ text " " <+> text "imported by:" <+> text l | l <- ps ]
65+
66+
-- | A message for a cyclical import, assuming the head of the path is the
67+
-- duplicate.
68+
cyclicalImportMsg :: ProjectConfigPath -> Doc
69+
cyclicalImportMsg path@(ProjectConfigPath (duplicate :| _)) =
70+
vcat
71+
[ text "cyclical import of" <+> text duplicate <> semi
72+
, nest 2 (docProjectConfigPath path)
73+
]
74+
75+
docProjectConfigPathFailReason :: VR -> ProjectConfigPath -> Doc
76+
docProjectConfigPathFailReason vr pcp
77+
| ProjectConfigPath (p :| []) <- pcp =
78+
constraint p
79+
| ProjectConfigPath (p :| ps) <- pcp = vcat
80+
[ constraint p
81+
, cat [nest 2 $ text "imported by:" <+> text l | l <- ps ]
82+
]
83+
where
84+
pathRequiresVersion p = text p <+> text "requires" <+> text (prettyShow vr)
85+
constraint p = parens $ text "constraint from" <+> pathRequiresVersion p
86+
87+
-- | The root of the path, the project itself.
88+
projectConfigPathRoot :: ProjectConfigPath -> FilePath
89+
projectConfigPathRoot (ProjectConfigPath xs) = last xs
90+
91+
-- | Used by some tests as a dummy "unused" project root.
92+
nullProjectConfigPath :: ProjectConfigPath
93+
nullProjectConfigPath = ProjectConfigPath $ "unused" :| []
94+
95+
-- | Check if the path has duplicates. A cycle of imports is not allowed. This
96+
-- check should only be done after the path has been canonicalized with
97+
-- @canonicalizeConfigPath@. This is because the import path may contain paths
98+
-- that are the same in relation to their importers but different in relation to
99+
-- the project root directory.
100+
isCyclicConfigPath :: ProjectConfigPath -> Bool
101+
isCyclicConfigPath (ProjectConfigPath p) = length p /= length (NE.nub p)
102+
103+
-- | Prepends the path of the importee to the importer path.
104+
consProjectConfigPath :: FilePath -> ProjectConfigPath -> ProjectConfigPath
105+
consProjectConfigPath p ps = ProjectConfigPath (p <| coerce ps)
106+
107+
-- | Make paths relative to the directory of the root of the project, not
108+
-- relative to the file they were imported from.
109+
makeRelativeConfigPath :: FilePath -> ProjectConfigPath -> ProjectConfigPath
110+
makeRelativeConfigPath dir (ProjectConfigPath p) =
111+
ProjectConfigPath
112+
$ (\segment -> (if isURI segment then segment else makeRelative dir segment))
113+
<$> p
114+
115+
-- | Normalizes and canonicalizes a path removing '.' and '..' indirections.
116+
-- Makes the path relative to the given directory (typically the project root)
117+
-- instead of relative to the file it was imported from.
118+
--
119+
-- It converts paths like this:
120+
-- @
121+
-- └─ hops-0.project
122+
-- └─ hops/hops-1.config
123+
-- └─ ../hops-2.config
124+
-- └─ hops/hops-3.config
125+
-- └─ ../hops-4.config
126+
-- └─ hops/hops-5.config
127+
-- └─ ../hops-6.config
128+
-- └─ hops/hops-7.config
129+
-- └─ ../hops-8.config
130+
-- └─ hops/hops-9.config
131+
-- @
132+
--
133+
-- Into paths like this:
134+
-- @
135+
-- └─ hops-0.project
136+
-- └─ hops/hops-1.config
137+
-- └─ hops-2.config
138+
-- └─ hops/hops-3.config
139+
-- └─ hops-4.config
140+
-- └─ hops/hops-5.config
141+
-- └─ hops-6.config
142+
-- └─ hops/hops-7.config
143+
-- └─ hops-8.config
144+
-- └─ hops/hops-9.config
145+
-- @
146+
--
147+
-- That way we have @hops-8.config@ instead of
148+
-- @./hops/../hops/../hops/../hops/../hops-8.config@.
149+
--
150+
-- Let's see how @canonicalizePath@ works that is used in the implementation
151+
-- then we'll see how @canonicalizeConfigPath@ works.
152+
--
153+
-- >>> let d = testDir
154+
-- >>> makeRelative d <$> canonicalizePath (d </> "hops/../hops/../hops/../hops/../hops-8.config")
155+
-- "hops-8.config"
156+
--
157+
-- >>> let d = testDir
158+
-- >>> p <- canonicalizeConfigPath d (ProjectConfigPath $ (d </> "hops/../hops/../hops/../hops/../hops-8.config") :| [])
159+
-- >>> render $ docProjectConfigPath p
160+
-- "hops-8.config"
161+
--
162+
-- >>> :{
163+
-- do
164+
-- let expected = unlines
165+
-- [ "hops/hops-9.config"
166+
-- , " imported by: hops-8.config"
167+
-- , " imported by: hops/hops-7.config"
168+
-- , " imported by: hops-6.config"
169+
-- , " imported by: hops/hops-5.config"
170+
-- , " imported by: hops-4.config"
171+
-- , " imported by: hops/hops-3.config"
172+
-- , " imported by: hops-2.config"
173+
-- , " imported by: hops/hops-1.config"
174+
-- , " imported by: hops-0.project"
175+
-- ]
176+
-- let d = testDir
177+
-- let configPath = ProjectConfigPath ("hops/hops-9.config" :|
178+
-- [ "../hops-8.config"
179+
-- , "hops/hops-7.config"
180+
-- , "../hops-6.config"
181+
-- , "hops/hops-5.config"
182+
-- , "../hops-4.config"
183+
-- , "hops/hops-3.config"
184+
-- , "../hops-2.config"
185+
-- , "hops/hops-1.config"
186+
-- , d </> "hops-0.project"])
187+
-- p <- canonicalizeConfigPath d configPath
188+
-- return $ expected == render (docProjectConfigPath p) ++ "\n"
189+
-- :}
190+
-- True
191+
canonicalizeConfigPath :: FilePath -> ProjectConfigPath -> IO ProjectConfigPath
192+
canonicalizeConfigPath d (ProjectConfigPath p) = do
193+
xs <- sequence $ NE.scanr (\importee -> (>>= \importer ->
194+
if isURI importee
195+
then pure importee
196+
else canonicalizePath $ d </> takeDirectory importer </> importee))
197+
(pure ".") p
198+
return . makeRelativeConfigPath d . ProjectConfigPath . NE.fromList $ NE.init xs
199+
200+
isURI :: FilePath -> Bool
201+
isURI = isJust . parseURI
202+
203+
-- $setup
204+
-- >>> import Data.List
205+
-- >>> testDir <- makeAbsolute =<< canonicalizePath "../cabal-testsuite/PackageTests/ConditionalAndImport"

cabal-install/src/Distribution/Client/ProjectConfig.hs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
module Distribution.Client.ProjectConfig
99
( -- * Types for project config
1010
ProjectConfig (..)
11+
, ProjectConfigToParse (..)
1112
, ProjectConfigBuildOnly (..)
1213
, ProjectConfigShared (..)
1314
, ProjectConfigProvenance (..)
@@ -57,6 +58,7 @@ module Distribution.Client.ProjectConfig
5758
) where
5859

5960
import Distribution.Client.Compat.Prelude
61+
import Text.PrettyPrint (render)
6062
import Prelude ()
6163

6264
import Distribution.Client.Glob
@@ -230,6 +232,8 @@ import System.IO
230232
, withBinaryFile
231233
)
232234

235+
import Distribution.Solver.Types.ProjectConfigPath
236+
233237
----------------------------------------
234238
-- Resolving configuration to settings
235239
--
@@ -748,7 +752,7 @@ readProjectFileSkeleton
748752
then do
749753
monitorFiles [monitorFileHashed extensionFile]
750754
pcs <- liftIO readExtensionFile
751-
monitorFiles $ map monitorFileHashed (projectSkeletonImports pcs)
755+
monitorFiles $ map monitorFileHashed (projectConfigPathRoot <$> projectSkeletonImports pcs)
752756
pure pcs
753757
else do
754758
monitorFiles [monitorNonExistentFile extensionFile]
@@ -758,7 +762,7 @@ readProjectFileSkeleton
758762

759763
readExtensionFile =
760764
reportParseResult verbosity extensionDescription extensionFile
761-
=<< parseProjectSkeleton distDownloadSrcDirectory httpTransport verbosity [] extensionFile
765+
=<< parseProject extensionFile distDownloadSrcDirectory httpTransport verbosity . ProjectConfigToParse
762766
=<< BS.readFile extensionFile
763767

764768
-- | Render the 'ProjectConfig' format.
@@ -795,7 +799,7 @@ readGlobalConfig verbosity configFileFlag = do
795799
reportParseResult :: Verbosity -> String -> FilePath -> OldParser.ParseResult ProjectConfigSkeleton -> IO ProjectConfigSkeleton
796800
reportParseResult verbosity _filetype filename (OldParser.ParseOk warnings x) = do
797801
unless (null warnings) $
798-
let msg = unlines (map (OldParser.showPWarning (intercalate ", " $ filename : projectSkeletonImports x)) warnings)
802+
let msg = unlines (map (OldParser.showPWarning (intercalate ", " $ filename : (projectConfigPathRoot <$> projectSkeletonImports x))) warnings)
799803
in warn verbosity msg
800804
return x
801805
reportParseResult verbosity filetype filename (OldParser.ParseFailed err) =
@@ -879,7 +883,7 @@ renderBadPackageLocations (BadPackageLocations provenance bpls)
879883

880884
renderExplicit =
881885
"When using configuration(s) from "
882-
++ intercalate ", " (mapMaybe getExplicit (Set.toList provenance))
886+
++ intercalate ", " (render . docProjectConfigPath <$> mapMaybe getExplicit (Set.toList provenance))
883887
++ ", the following errors occurred:\n"
884888
++ renderErrors renderBadPackageLocation
885889

0 commit comments

Comments
 (0)