From 124d47163bde123c66543e78327f7c1c5aaa27fd Mon Sep 17 00:00:00 2001
From: Phil de Joux <philderbeast@gmail.com>
Date: Mon, 11 Nov 2024 07:45:55 -0500
Subject: [PATCH 1/2] Add Ord instance for ProjectConfigPath

- Consider URI in Ord instance
- Use <> in Ord instance of ProjectConfigPath
- Add unconsProjectConfigPath
- Add docProjectConfigFiles
- Remove docProjectConfigPaths
- Clarify sorting haddocks for Ord ProjectConfigPath
---
 .../Solver/Types/ProjectConfigPath.hs         | 61 ++++++++++++++++---
 .../src/Distribution/Client/ProjectConfig.hs  |  2 +-
 changelog.d/pr-10546                          |  9 +++
 3 files changed, 62 insertions(+), 10 deletions(-)
 create mode 100644 changelog.d/pr-10546

diff --git a/cabal-install-solver/src/Distribution/Solver/Types/ProjectConfigPath.hs b/cabal-install-solver/src/Distribution/Solver/Types/ProjectConfigPath.hs
index bb1a81bc1b5..84375b0f4de 100644
--- a/cabal-install-solver/src/Distribution/Solver/Types/ProjectConfigPath.hs
+++ b/cabal-install-solver/src/Distribution/Solver/Types/ProjectConfigPath.hs
@@ -1,4 +1,5 @@
 {-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE ViewPatterns #-}
 
 module Distribution.Solver.Types.ProjectConfigPath
     (
@@ -7,10 +8,11 @@ module Distribution.Solver.Types.ProjectConfigPath
     , projectConfigPathRoot
     , nullProjectConfigPath
     , consProjectConfigPath
+    , unconsProjectConfigPath
 
     -- * Messages
     , docProjectConfigPath
-    , docProjectConfigPaths
+    , docProjectConfigFiles
     , cyclicalImportMsg
     , docProjectConfigPathFailReason
 
@@ -21,17 +23,19 @@ module Distribution.Solver.Types.ProjectConfigPath
     ) where
 
 import Distribution.Solver.Compat.Prelude hiding (toList, (<>))
+import qualified Distribution.Solver.Compat.Prelude as P ((<>))
 import Prelude (sequence)
 
 import Data.Coerce (coerce)
 import Data.List.NonEmpty ((<|))
-import Network.URI (parseURI)
+import Network.URI (parseURI, parseAbsoluteURI)
 import System.Directory
 import System.FilePath
 import qualified Data.List.NonEmpty as NE
 import Distribution.Solver.Modular.Version (VR)
 import Distribution.Pretty (prettyShow)
 import Text.PrettyPrint
+import Distribution.Simple.Utils (ordNub)
 
 -- | Path to a configuration file, either a singleton project root, or a longer
 -- list representing a path to an import.  The path is a non-empty list that we
@@ -45,7 +49,41 @@ import Text.PrettyPrint
 -- List elements are relative to each other but once canonicalized, elements are
 -- relative to the directory of the project root.
 newtype ProjectConfigPath = ProjectConfigPath (NonEmpty FilePath)
-    deriving (Eq, Ord, Show, Generic)
+    deriving (Eq, Show, Generic)
+
+-- | Sorts URIs after local file paths and longer file paths after shorter ones
+-- as measured by the number of path segments. If still equal, then sorting is
+-- lexical.
+--
+-- The project itself, a single element root path, compared to any of the
+-- configuration paths it imports, should always sort first. Comparing one
+-- project root path against another is done lexically.
+instance Ord ProjectConfigPath where
+    compare pa@(ProjectConfigPath (NE.toList -> as)) pb@(ProjectConfigPath (NE.toList -> bs)) =
+        case (as, bs) of
+            -- There should only ever be one root project path, only one path
+            -- with length 1. Comparing it to itself should be EQ. Don't assume
+            -- this though, do a comparison anyway when both sides have length
+            -- 1.  The root path, the project itself, should always be the first
+            -- path in a sorted listing.
+            ([a], [b]) -> compare a b
+            ([_], _) -> LT
+            (_, [_]) -> GT
+
+            (a:_, b:_) -> case (parseAbsoluteURI a, parseAbsoluteURI b) of
+                (Just ua, Just ub) -> compare ua ub P.<> compare aImporters bImporters
+                (Just _, Nothing) -> GT
+                (Nothing, Just _) -> LT
+                (Nothing, Nothing) -> compare (splitPath a) (splitPath b) P.<> compare aImporters bImporters
+            _ ->
+                compare (length as) (length bs)
+                P.<> compare (length aPaths) (length bPaths)
+                P.<> compare aPaths bPaths
+        where
+            aPaths = splitPath <$> as
+            bPaths = splitPath <$> bs
+            aImporters = snd $ unconsProjectConfigPath pa
+            bImporters = snd $ unconsProjectConfigPath pb
 
 instance Binary ProjectConfigPath
 instance Structured ProjectConfigPath
@@ -95,15 +133,16 @@ docProjectConfigPath (ProjectConfigPath (p :| ps)) = vcat $
 --              , ProjectConfigPath ("project-cabal/pkgs/integration-tests.config" :| ["project-cabal/pkgs.config","cabal.project"])
 --              , ProjectConfigPath ("project-cabal/pkgs/tests.config" :| ["project-cabal/pkgs.config","cabal.project"])
 --              ]
---     return . render $ docProjectConfigPaths ps
+--     return . render $ docProjectConfigFiles ps
 -- :}
 -- "- cabal.project\n- project-cabal/constraints.config\n- project-cabal/ghc-latest.config\n- project-cabal/ghc-options.config\n- project-cabal/pkgs.config\n- project-cabal/pkgs/benchmarks.config\n- project-cabal/pkgs/buildinfo.config\n- project-cabal/pkgs/cabal.config\n- project-cabal/pkgs/install.config\n- project-cabal/pkgs/integration-tests.config\n- project-cabal/pkgs/tests.config"
-docProjectConfigPaths :: [ProjectConfigPath] -> Doc
-docProjectConfigPaths ps = vcat
-    [ text "-" <+> text p | ProjectConfigPath (p :| _) <- ps ]
+docProjectConfigFiles :: [ProjectConfigPath] -> Doc
+docProjectConfigFiles ps = vcat
+    [ text "-" <+> text p
+    | p <- ordNub [ p | ProjectConfigPath (p :| _) <- ps ]
+    ]
 
--- | A message for a cyclical import, assuming the head of the path is the
--- duplicate.
+-- | A message for a cyclical import, a "cyclical import of".
 cyclicalImportMsg :: ProjectConfigPath -> Doc
 cyclicalImportMsg path@(ProjectConfigPath (duplicate :| _)) =
     vcat
@@ -148,6 +187,10 @@ isTopLevelConfigPath (ProjectConfigPath p) = NE.length p == 1
 consProjectConfigPath :: FilePath -> ProjectConfigPath -> ProjectConfigPath
 consProjectConfigPath p ps = ProjectConfigPath (p <| coerce ps)
 
+-- | Split the path into the importee and the importer path.
+unconsProjectConfigPath :: ProjectConfigPath -> (FilePath, Maybe ProjectConfigPath)
+unconsProjectConfigPath ps = fmap ProjectConfigPath <$> NE.uncons (coerce ps)
+
 -- | Make paths relative to the directory of the root of the project, not
 -- relative to the file they were imported from.
 makeRelativeConfigPath :: FilePath -> ProjectConfigPath -> ProjectConfigPath
diff --git a/cabal-install/src/Distribution/Client/ProjectConfig.hs b/cabal-install/src/Distribution/Client/ProjectConfig.hs
index eea6b958b70..5f31dc0fab5 100644
--- a/cabal-install/src/Distribution/Client/ProjectConfig.hs
+++ b/cabal-install/src/Distribution/Client/ProjectConfig.hs
@@ -951,7 +951,7 @@ renderBadPackageLocations (BadPackageLocations provenance bpls)
 
     renderExplicit =
       "When using configuration from:\n"
-        ++ render (nest 2 . docProjectConfigPaths $ mapMaybe getExplicit (Set.toList provenance))
+        ++ render (nest 2 . docProjectConfigFiles $ mapMaybe getExplicit (Set.toList provenance))
         ++ "\nThe following errors occurred:\n"
         ++ render (nest 2 $ vcat ((text "-" <+>) . text <$> map renderBadPackageLocation bpls))
 
diff --git a/changelog.d/pr-10546 b/changelog.d/pr-10546
new file mode 100644
index 00000000000..1851e21c0e4
--- /dev/null
+++ b/changelog.d/pr-10546
@@ -0,0 +1,9 @@
+---
+synopsis: Add an Ord instance for ProjectConfigPath
+packages: [cabal-install-solver]
+prs: 10546
+---
+
+Add an `Ord` instance for `ProjectConfigPath` that sorts URIs after local paths
+and longer paths after shorter ones. Deduplicate the printing of "Configuration is
+affected by the following files" messages.

From 73a13f125a1c93b1e1500346713f69152ce4993a Mon Sep 17 00:00:00 2001
From: Phil de Joux <philderbeast@gmail.com>
Date: Mon, 9 Dec 2024 19:51:54 -0500
Subject: [PATCH 2/2] Add test projects

- Add woops test
- Regen expected output without duplication
- Add "when using config from" tests
- Remove unnecessary normalizeWindowsOutput
- Change assertion message
- Match on (\n|\r\n) for line endings
- Note /tmp/cabal-testsuite-*/ not seen on Windows
- Always have the project itself sort first
- Use with-ghc.config with woops project
- Remove docProjectConfigPaths
- docProjectConfigFiles is the better name when not reporting "imported by"
- Use --dry-run for config listing tests
- Only use woops project once in tests
- Don't use same project twice in tests
- Put dedup test into its own folder
- Simplify the project file names
- Can be done now that the test is in its own folder
- Move using config dedup test to ProjectImport dir
- Remove an additional test on yops project
- checking "using config from message" without URI imports
- Add simple test for changelog
- Better explain the message changes
- Redo the test so the project doesn't sort 1st
- Don't need to specify default project explicitly
- Add a z-empty.config lexically sorted last
---
 .../ConditionalAndImport/cabal.out            |  4 +-
 .../ConditionalAndImport/with-ghc.config      |  7 ++
 .../DedupUsingConfigFromComplex/0.config      |  7 ++
 .../DedupUsingConfigFromComplex/2.config      |  2 +
 .../DedupUsingConfigFromComplex/4.config      |  2 +
 .../DedupUsingConfigFromComplex/6.config      |  2 +
 .../DedupUsingConfigFromComplex/8.config      |  2 +
 .../DedupUsingConfigFromComplex/cabal.out     |  3 +
 .../DedupUsingConfigFromComplex/cabal.test.hs | 42 ++++++++++
 .../DedupUsingConfigFromComplex/cfg/1.config  |  2 +
 .../DedupUsingConfigFromComplex/cfg/3.config  |  2 +
 .../DedupUsingConfigFromComplex/cfg/5.config  |  2 +
 .../DedupUsingConfigFromComplex/cfg/7.config  |  2 +
 .../DedupUsingConfigFromComplex/cfg/9.config  |  2 +
 .../no-pkg-1/README.md                        |  2 +
 .../no-pkg-3/README.md                        |  2 +
 .../no-pkgs.project                           |  7 ++
 .../with-ghc.config                           |  7 ++
 .../a-very-extra.config                       |  2 +
 .../an-extra.config                           |  2 +
 .../DedupUsingConfigFromSimple/cabal.out      |  1 +
 .../DedupUsingConfigFromSimple/cabal.project  |  9 +++
 .../DedupUsingConfigFromSimple/cabal.test.hs  | 24 ++++++
 .../with-ghc.config                           |  7 ++
 .../DedupUsingConfigFromSimple/z-empty.config |  1 +
 changelog.d/pr-10546                          | 81 ++++++++++++++++++-
 26 files changed, 220 insertions(+), 6 deletions(-)
 create mode 100644 cabal-testsuite/PackageTests/ConditionalAndImport/with-ghc.config
 create mode 100644 cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromComplex/0.config
 create mode 100644 cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromComplex/2.config
 create mode 100644 cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromComplex/4.config
 create mode 100644 cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromComplex/6.config
 create mode 100644 cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromComplex/8.config
 create mode 100644 cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromComplex/cabal.out
 create mode 100644 cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromComplex/cabal.test.hs
 create mode 100644 cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromComplex/cfg/1.config
 create mode 100644 cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromComplex/cfg/3.config
 create mode 100644 cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromComplex/cfg/5.config
 create mode 100644 cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromComplex/cfg/7.config
 create mode 100644 cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromComplex/cfg/9.config
 create mode 100644 cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromComplex/no-pkg-1/README.md
 create mode 100644 cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromComplex/no-pkg-3/README.md
 create mode 100644 cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromComplex/no-pkgs.project
 create mode 100644 cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromComplex/with-ghc.config
 create mode 100644 cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromSimple/a-very-extra.config
 create mode 100644 cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromSimple/an-extra.config
 create mode 100644 cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromSimple/cabal.out
 create mode 100644 cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromSimple/cabal.project
 create mode 100644 cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromSimple/cabal.test.hs
 create mode 100644 cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromSimple/with-ghc.config
 create mode 100644 cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromSimple/z-empty.config

diff --git a/cabal-testsuite/PackageTests/ConditionalAndImport/cabal.out b/cabal-testsuite/PackageTests/ConditionalAndImport/cabal.out
index c2690ee4366..fd408a95505 100644
--- a/cabal-testsuite/PackageTests/ConditionalAndImport/cabal.out
+++ b/cabal-testsuite/PackageTests/ConditionalAndImport/cabal.out
@@ -67,9 +67,9 @@ cyclical import of cyclical-2-out-out-self-b.config;
 # checking that cyclical check doesn't false-positive on same file names in different folders; hoping within a folder and then into a subfolder
 # cabal v2-build
 Configuration is affected by the following files:
+- noncyclical-same-filename-a.project
 - noncyclical-same-filename-a.config
     imported by: noncyclical-same-filename-a.project
-- noncyclical-same-filename-a.project
 - same-filename/noncyclical-same-filename-a.config
     imported by: noncyclical-same-filename-a.config
     imported by: noncyclical-same-filename-a.project
@@ -83,10 +83,10 @@ Building library for my-0.1...
 # checking that cyclical check doesn't false-positive on same file names in different folders; hoping into a subfolder and then back out again
 # cabal v2-build
 Configuration is affected by the following files:
+- noncyclical-same-filename-b.project
 - noncyclical-same-filename-b.config
     imported by: same-filename/noncyclical-same-filename-b.config
     imported by: noncyclical-same-filename-b.project
-- noncyclical-same-filename-b.project
 - same-filename/noncyclical-same-filename-b.config
     imported by: noncyclical-same-filename-b.project
 Up to date
diff --git a/cabal-testsuite/PackageTests/ConditionalAndImport/with-ghc.config b/cabal-testsuite/PackageTests/ConditionalAndImport/with-ghc.config
new file mode 100644
index 00000000000..140a00be1b9
--- /dev/null
+++ b/cabal-testsuite/PackageTests/ConditionalAndImport/with-ghc.config
@@ -0,0 +1,7 @@
+-- WARNING: Override the `with-compiler: ghc-x.y.z` of the stackage import, of
+-- https://www.stackage.org/nightly-yyyy-mm-dd/cabal.config. Otherwise tests
+-- will fail with:
+--   -Error: [Cabal-5490]
+--   -Cannot find the program 'ghc'. User-specified path 'ghc-x.y.z' does not
+--    refer to an executable and the program is not on the system path.
+with-compiler: ghc
diff --git a/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromComplex/0.config b/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromComplex/0.config
new file mode 100644
index 00000000000..05195095cb0
--- /dev/null
+++ b/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromComplex/0.config
@@ -0,0 +1,7 @@
+import: cfg/1.config
+import: cfg/3.config
+import: cfg/5.config
+import: cfg/7.config
+import: cfg/9.config
+
+import: with-ghc.config
diff --git a/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromComplex/2.config b/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromComplex/2.config
new file mode 100644
index 00000000000..80143e396c8
--- /dev/null
+++ b/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromComplex/2.config
@@ -0,0 +1,2 @@
+import: cfg/3.config
+import: https://www.stackage.org/lts-21.25/cabal.config
diff --git a/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromComplex/4.config b/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromComplex/4.config
new file mode 100644
index 00000000000..8b84982cc3e
--- /dev/null
+++ b/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromComplex/4.config
@@ -0,0 +1,2 @@
+import: cfg/5.config
+import: https://www.stackage.org/lts-21.25/cabal.config
diff --git a/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromComplex/6.config b/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromComplex/6.config
new file mode 100644
index 00000000000..43ce76e8766
--- /dev/null
+++ b/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromComplex/6.config
@@ -0,0 +1,2 @@
+import: cfg/7.config
+import: https://www.stackage.org/lts-21.25/cabal.config
diff --git a/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromComplex/8.config b/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromComplex/8.config
new file mode 100644
index 00000000000..28d0551160f
--- /dev/null
+++ b/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromComplex/8.config
@@ -0,0 +1,2 @@
+import: cfg/9.config
+import: https://www.stackage.org/lts-21.25/cabal.config
diff --git a/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromComplex/cabal.out b/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromComplex/cabal.out
new file mode 100644
index 00000000000..437612a2eca
--- /dev/null
+++ b/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromComplex/cabal.out
@@ -0,0 +1,3 @@
+# checking "using config from message" with URI imports
+# cabal v2-build
+# checking that package directories and locations are reported in order
diff --git a/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromComplex/cabal.test.hs b/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromComplex/cabal.test.hs
new file mode 100644
index 00000000000..e354b356d7f
--- /dev/null
+++ b/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromComplex/cabal.test.hs
@@ -0,0 +1,42 @@
+import Test.Cabal.Prelude
+
+main = cabalTest . recordMode RecordMarked $ do
+  let log = recordHeader . pure
+
+  log "checking \"using config from message\" with URI imports"
+  out <- fails $ cabal' "v2-build" [ "all", "--dry-run", "--project-file=no-pkgs.project" ]
+
+  -- Use assertRegex when the output is tainted by the temp directory, like
+  -- this:
+  --
+  --   When using configuration from:
+  --   - /tmp/cabal-testsuite-282695/cabal.project
+  --   - /tmp/cabal-testsuite-282695/2.config etc
+  assertRegex
+    "Project configuration with URI imports is listed in full"
+    "When using configuration from:(\n|\r\n) \
+      \ .*no-pkgs\\.project(\n|\r\n) \
+      \ .*0\\.config(\n|\r\n) \
+      \ .*2\\.config(\n|\r\n) \
+      \ .*4\\.config(\n|\r\n) \
+      \ .*6\\.config(\n|\r\n) \
+      \ .*8\\.config(\n|\r\n) \
+      \ .*1\\.config(\n|\r\n) \
+      \ .*3\\.config(\n|\r\n) \
+      \ .*5\\.config(\n|\r\n) \
+      \ .*7\\.config(\n|\r\n) \
+      \ .*9\\.config(\n|\r\n) \
+      \ .*with-ghc\\.config(\n|\r\n) \
+      \ .*https://www.stackage.org/lts-21.25/cabal.config(\n|\r\n)"
+    out
+
+  log "checking that package directories and locations are reported in order"
+  assertOutputContains
+    "The following errors occurred: \
+    \  - The package directory 'no-pkg-1' does not contain any .cabal file. \
+    \  - The package location 'no-pkg-2-dir' does not exist. \
+    \  - The package directory 'no-pkg-3' does not contain any .cabal file. \
+    \  - The package location 'no-pkg-4-dir' does not exist."
+    out
+
+  return ()
diff --git a/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromComplex/cfg/1.config b/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromComplex/cfg/1.config
new file mode 100644
index 00000000000..126a94da61d
--- /dev/null
+++ b/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromComplex/cfg/1.config
@@ -0,0 +1,2 @@
+import: ../2.config
+import: https://www.stackage.org/lts-21.25/cabal.config
diff --git a/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromComplex/cfg/3.config b/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromComplex/cfg/3.config
new file mode 100644
index 00000000000..f40b183e472
--- /dev/null
+++ b/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromComplex/cfg/3.config
@@ -0,0 +1,2 @@
+import: ../4.config
+import: https://www.stackage.org/lts-21.25/cabal.config
diff --git a/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromComplex/cfg/5.config b/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromComplex/cfg/5.config
new file mode 100644
index 00000000000..7f579a54345
--- /dev/null
+++ b/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromComplex/cfg/5.config
@@ -0,0 +1,2 @@
+import: ../6.config
+import: https://www.stackage.org/lts-21.25/cabal.config
diff --git a/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromComplex/cfg/7.config b/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromComplex/cfg/7.config
new file mode 100644
index 00000000000..7cd98c9c244
--- /dev/null
+++ b/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromComplex/cfg/7.config
@@ -0,0 +1,2 @@
+import: ../8.config
+import: https://www.stackage.org/lts-21.25/cabal.config
diff --git a/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromComplex/cfg/9.config b/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromComplex/cfg/9.config
new file mode 100644
index 00000000000..44d1cc5e562
--- /dev/null
+++ b/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromComplex/cfg/9.config
@@ -0,0 +1,2 @@
+-- No imports here
+import: https://www.stackage.org/lts-21.25/cabal.config
diff --git a/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromComplex/no-pkg-1/README.md b/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromComplex/no-pkg-1/README.md
new file mode 100644
index 00000000000..ba73c42531f
--- /dev/null
+++ b/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromComplex/no-pkg-1/README.md
@@ -0,0 +1,2 @@
+There's intentionally no package here but the directory for the package exists
+so that the project can find it.
diff --git a/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromComplex/no-pkg-3/README.md b/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromComplex/no-pkg-3/README.md
new file mode 100644
index 00000000000..ba73c42531f
--- /dev/null
+++ b/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromComplex/no-pkg-3/README.md
@@ -0,0 +1,2 @@
+There's intentionally no package here but the directory for the package exists
+so that the project can find it.
diff --git a/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromComplex/no-pkgs.project b/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromComplex/no-pkgs.project
new file mode 100644
index 00000000000..0d723a9e298
--- /dev/null
+++ b/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromComplex/no-pkgs.project
@@ -0,0 +1,7 @@
+packages:
+    no-pkg-1
+    no-pkg-2-dir
+    no-pkg-3
+    no-pkg-4-dir
+
+import: 0.config
diff --git a/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromComplex/with-ghc.config b/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromComplex/with-ghc.config
new file mode 100644
index 00000000000..140a00be1b9
--- /dev/null
+++ b/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromComplex/with-ghc.config
@@ -0,0 +1,7 @@
+-- WARNING: Override the `with-compiler: ghc-x.y.z` of the stackage import, of
+-- https://www.stackage.org/nightly-yyyy-mm-dd/cabal.config. Otherwise tests
+-- will fail with:
+--   -Error: [Cabal-5490]
+--   -Cannot find the program 'ghc'. User-specified path 'ghc-x.y.z' does not
+--    refer to an executable and the program is not on the system path.
+with-compiler: ghc
diff --git a/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromSimple/a-very-extra.config b/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromSimple/a-very-extra.config
new file mode 100644
index 00000000000..c0f8dc3cc73
--- /dev/null
+++ b/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromSimple/a-very-extra.config
@@ -0,0 +1,2 @@
+import: https://www.stackage.org/lts-21.25/cabal.config
+import: https://www.stackage.org/lts-21.25/cabal.config
diff --git a/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromSimple/an-extra.config b/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromSimple/an-extra.config
new file mode 100644
index 00000000000..c0f8dc3cc73
--- /dev/null
+++ b/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromSimple/an-extra.config
@@ -0,0 +1,2 @@
+import: https://www.stackage.org/lts-21.25/cabal.config
+import: https://www.stackage.org/lts-21.25/cabal.config
diff --git a/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromSimple/cabal.out b/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromSimple/cabal.out
new file mode 100644
index 00000000000..92fd8204a40
--- /dev/null
+++ b/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromSimple/cabal.out
@@ -0,0 +1 @@
+# cabal v2-build
diff --git a/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromSimple/cabal.project b/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromSimple/cabal.project
new file mode 100644
index 00000000000..32308f3491f
--- /dev/null
+++ b/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromSimple/cabal.project
@@ -0,0 +1,9 @@
+packages: no-pkg-dir
+import: z-empty.config
+import: an-extra.config
+import: an-extra.config
+import: a-very-extra.config
+import: a-very-extra.config
+import: https://www.stackage.org/lts-21.25/cabal.config
+import: https://www.stackage.org/lts-21.25/cabal.config
+import: with-ghc.config
diff --git a/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromSimple/cabal.test.hs b/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromSimple/cabal.test.hs
new file mode 100644
index 00000000000..cf37d1621a6
--- /dev/null
+++ b/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromSimple/cabal.test.hs
@@ -0,0 +1,24 @@
+import Test.Cabal.Prelude
+
+main = cabalTest . recordMode RecordMarked $ do
+  let log = recordHeader . pure
+
+  out <- fails $ cabal' "v2-build" [ "all", "--dry-run" ]
+
+  -- Use assertRegex when the output is tainted by the temp directory, like
+  -- this:
+  --
+  --   When using configuration from:
+  --   - /tmp/cabal-testsuite-282695/cabal.project
+  assertRegex
+    "Project configuration is listed in full and deduplicated"
+    "When using configuration from:(\n|\r\n) \
+      \ .*cabal\\.project(\n|\r\n) \
+      \ .*a-very-extra\\.config(\n|\r\n) \
+      \ .*an-extra\\.config(\n|\r\n) \
+      \ .*with-ghc\\.config(\n|\r\n) \
+      \ .*z-empty\\.config(\n|\r\n) \
+      \ .*https://www.stackage.org/lts-21.25/cabal.config(\n|\r\n)"
+    out
+
+  return ()
diff --git a/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromSimple/with-ghc.config b/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromSimple/with-ghc.config
new file mode 100644
index 00000000000..140a00be1b9
--- /dev/null
+++ b/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromSimple/with-ghc.config
@@ -0,0 +1,7 @@
+-- WARNING: Override the `with-compiler: ghc-x.y.z` of the stackage import, of
+-- https://www.stackage.org/nightly-yyyy-mm-dd/cabal.config. Otherwise tests
+-- will fail with:
+--   -Error: [Cabal-5490]
+--   -Cannot find the program 'ghc'. User-specified path 'ghc-x.y.z' does not
+--    refer to an executable and the program is not on the system path.
+with-compiler: ghc
diff --git a/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromSimple/z-empty.config b/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromSimple/z-empty.config
new file mode 100644
index 00000000000..771bb389fde
--- /dev/null
+++ b/cabal-testsuite/PackageTests/ProjectImport/DedupUsingConfigFromSimple/z-empty.config
@@ -0,0 +1 @@
+-- This file is intentionally empty, just this comment.
diff --git a/changelog.d/pr-10546 b/changelog.d/pr-10546
index 1851e21c0e4..db0951505f0 100644
--- a/changelog.d/pr-10546
+++ b/changelog.d/pr-10546
@@ -1,9 +1,82 @@
 ---
-synopsis: Add an Ord instance for ProjectConfigPath
+synopsis: Deduplicate "using configuration from" message
 packages: [cabal-install-solver]
 prs: 10546
 ---
 
-Add an `Ord` instance for `ProjectConfigPath` that sorts URIs after local paths
-and longer paths after shorter ones. Deduplicate the printing of "Configuration is
-affected by the following files" messages.
+## Using Configuration From Message Changes
+
+Deduplicates and sorts the list of configuration files and URIs printed with the
+"using configuration from" message. This message is shown when there's a build
+failure. We can trigger that message by using a non-existant package in the
+project, "no-pkg-dir".
+
+If an import is repeated in a `.project` or `.config` file it only imported once
+but if the same import is made from an imported file then it was being repeated
+in the message. Additional problems were not showing the project first and
+mixing configuration files and URIs together.
+
+* The test set up:
+
+    ```
+    $ cat cabal.project
+    cat cabal.project
+    packages: no-pkg-dir
+    import: z-empty.config
+    import: an-extra.config
+    import: an-extra.config
+    import: a-very-extra.config
+    import: a-very-extra.config
+    import: https://www.stackage.org/lts-21.25/cabal.config
+    import: https://www.stackage.org/lts-21.25/cabal.config
+
+    $ cat an-extra.config
+    import: https://www.stackage.org/lts-21.25/cabal.config
+    import: https://www.stackage.org/lts-21.25/cabal.config
+
+    $ cat a-very-extra.config
+    import: https://www.stackage.org/lts-21.25/cabal.config
+    import: https://www.stackage.org/lts-21.25/cabal.config
+
+    $ cat z-empty.config 
+    - This file is intentionally empty, just this comment.
+    ```
+
+* Before the fix:
+
+    ```
+    $ ~/.ghcup/bin/cabal-3.12.1.0 build all --dry-run
+    When using configuration from:
+    - a-very-extra.config
+    - an-extra.config
+    - cabal.project
+    - https://www.stackage.org/lts-21.25/cabal.config
+    - https://www.stackage.org/lts-21.25/cabal.config
+    - https://www.stackage.org/lts-21.25/cabal.config
+    - z-empty.config
+    The following errors occurred:
+    - The package location 'no-pkg-dir' does not exist.
+    ```
+
+* After the fix:
+
+    ```
+    $ cabal build all --dry-run
+    When using configuration from:
+    - cabal.project
+    - a-very-extra.config
+    - an-extra.config
+    - z-empty.config
+    - https://www.stackage.org/lts-21.25/cabal.config
+    The following errors occurred:
+    - The package location 'no-pkg-dir' does not exist.
+    ```
+
+## Ord ProjectConfigPath Instance Changes
+
+Adds a custom `Ord` instance for `ProjectConfigPath` that sorts URIs after local
+file paths and longer file paths after shorter ones as measured by the number of
+path segments. If still equal, then sorting is lexical.  The project itself, a
+single element root path, compared to any of the configuration paths it imports,
+should always sort first. Comparing one project root path against another is
+done lexically.