Skip to content

Commit 36e2364

Browse files
authored
Merge pull request #10871 from haskell/mergify/bp/3.14/pr-10728
Backport #10728: Fix file+noindex URI usage on Windows
2 parents 8c7b3c0 + bbbec9b commit 36e2364

File tree

11 files changed

+115
-19
lines changed

11 files changed

+115
-19
lines changed

Cabal-syntax/src/Distribution/Utils/Path.hs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ module Distribution.Utils.Path
6767

6868
-- ** Module names
6969
, moduleNameSymbolicPath
70+
71+
-- * Windows
72+
, asPosixPath
7073
) where
7174

7275
import Distribution.Compat.Prelude
@@ -86,6 +89,8 @@ import qualified Distribution.Compat.CharParsing as P
8689

8790
import qualified System.Directory as Directory
8891
import qualified System.FilePath as FilePath
92+
import qualified System.FilePath.Posix as Posix
93+
import qualified System.FilePath.Windows as Windows
8994

9095
import Data.Kind
9196
( Type
@@ -531,3 +536,38 @@ data Response
531536
--
532537
-- See Note [Symbolic paths] in Distribution.Utils.Path.
533538
data PkgConf
539+
540+
-------------------------------------------------------------------------------
541+
542+
-- * Windows utils
543+
544+
-------------------------------------------------------------------------------
545+
546+
-- | Sometimes we need to represent a Windows path (that might have been
547+
-- normalized) as a POSIX path, for example in URIs, as that is what
548+
-- @network-uri@ understands. Furthermore they need to use the @\\\\.\\@ DOS
549+
-- device syntax or otherwise the filepath will be unusable.
550+
--
551+
-- >>> import Network.URI
552+
-- >>> uriPath <$> parseURI "file+noindex://C:/foo.txt"
553+
-- Just "/foo.txt"
554+
-- >>> parseURI "file+noindex://C:\foo.txt"
555+
-- Nothing
556+
-- >>> uriPath <$> parseURI "file+noindex:///C:/foo.txt"
557+
-- Just "/C:/foo.txt"
558+
-- >>> uriPath <$> parseURI "file+noindex:////./C:/foo.txt"
559+
-- Just "//./C:/foo.txt"
560+
--
561+
-- Out of the ones above, only the last one can be used from anywhere in the
562+
-- system, after normalization into @"\\\\.\\C:/foo.txt"@ (see filepath#247 for
563+
-- why there is a forward slash there):
564+
--
565+
-- >>> import Network.URI
566+
-- >>> import qualified System.FilePath.Windows as Windows
567+
-- >>> Windows.normalise . uriPath <$> parseURI "file+noindex:////./C:/foo.txt"
568+
-- Just "\\\\.\\C:/foo.txt"
569+
asPosixPath :: FilePath -> FilePath
570+
asPosixPath p =
571+
-- We don't use 'isPathSeparator' because @Windows.isPathSeparator
572+
-- Posix.pathSeparator == True@.
573+
[if x == Windows.pathSeparator then Posix.pathSeparator else x | x <- p]

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,8 @@ ghcid-cli :
111111
ghcid -c 'cabal repl cabal-install'
112112

113113
.PHONY: doctest
114-
doctest :
115-
cd Cabal-syntax && $(DOCTEST)
114+
doctest:
115+
cd Cabal-syntax && $(DOCTEST) --build-depends=network-uri
116116
cd Cabal-described && $(DOCTEST)
117117
cd Cabal && $(DOCTEST)
118118
cd cabal-install-solver && $(DOCTEST)

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,8 @@ import System.Directory
226226
, renameFile
227227
)
228228
import System.FilePath
229-
( takeDirectory
229+
( normalise
230+
, takeDirectory
230231
, (<.>)
231232
, (</>)
232233
)
@@ -1679,7 +1680,14 @@ postProcessRepo lineno reponameStr repo0 = do
16791680
-- Note: the trailing colon is important
16801681
"file+noindex:" -> do
16811682
let uri = remoteRepoURI repo0
1682-
return $ Left $ LocalRepo reponame (uriPath uri) (uriFragment uri == "#shared-cache")
1683+
return $
1684+
Left $
1685+
LocalRepo
1686+
reponame
1687+
-- Normalization of Windows paths that use @//./@ does not fully
1688+
-- normalize the path (see filepath#247), but it is still usable.
1689+
(normalise (uriPath uri))
1690+
(uriFragment uri == "#shared-cache")
16831691
_ -> do
16841692
let repo = repo0{remoteRepoName = reponame}
16851693

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

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ import Network.URI
5757
, uriScheme
5858
)
5959
import System.FilePath
60-
( (</>)
60+
( isAbsolute
61+
, (</>)
6162
)
6263

6364
import qualified Distribution.Client.Security.DNS as Sec.DNS
@@ -69,8 +70,6 @@ import qualified Hackage.Security.Client.Repository.Remote as Sec.Remote
6970
import qualified Hackage.Security.Util.Path as Sec
7071
import qualified Hackage.Security.Util.Pretty as Sec
7172

72-
import qualified System.FilePath.Posix as FilePath.Posix
73-
7473
-- ------------------------------------------------------------
7574

7675
-- * Global flags
@@ -192,7 +191,7 @@ withRepoContext'
192191
ignoreExpiry
193192
extraPaths = \callback -> do
194193
for_ localNoIndexRepos $ \local ->
195-
unless (FilePath.Posix.isAbsolute (localRepoPath local)) $
194+
unless (isAbsolute (localRepoPath local)) $
196195
warn verbosity $
197196
"file+noindex " ++ unRepoName (localRepoName local) ++ " repository path is not absolute; this is fragile, and not recommended"
198197

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -456,7 +456,7 @@ readRepoIndex verbosity repoCtxt repo idxState =
456456
RepoSecure{..} -> warn verbosity $ exceptionMessageCabalInstall $ MissingPackageList repoRemote
457457
RepoLocalNoIndex local _ ->
458458
warn verbosity $
459-
"Error during construction of local+noindex "
459+
"Error during construction of file+noindex "
460460
++ unRepoName (localRepoName local)
461461
++ " repository index: "
462462
++ show e
@@ -516,7 +516,7 @@ whenCacheOutOfDate index action = do
516516
then action
517517
else
518518
if localNoIndex index
519-
then return () -- TODO: don't update cache for local+noindex repositories
519+
then return () -- TODO: don't update cache for file+noindex repositories
520520
else do
521521
indexTime <- getModTime $ indexFile index
522522
cacheTime <- getModTime $ cacheFile index

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

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ import Distribution.Simple.Command
173173
, option
174174
, reqArg'
175175
)
176-
import Distribution.System (Arch, OS)
176+
import Distribution.System (Arch, OS (Windows), buildOS)
177177
import Distribution.Types.PackageVersionConstraint
178178
( PackageVersionConstraint
179179
)
@@ -185,7 +185,7 @@ import Distribution.Utils.Path hiding
185185
import qualified Data.ByteString.Char8 as BS
186186
import qualified Data.Map as Map
187187
import qualified Data.Set as Set
188-
import Network.URI (URI (..), parseURI)
188+
import Network.URI (URI (..), nullURIAuth, parseURI)
189189
import System.Directory (createDirectoryIfMissing, makeAbsolute)
190190
import System.FilePath (isAbsolute, isPathSeparator, makeValid, splitFileName, (</>))
191191
import Text.PrettyPrint
@@ -2043,9 +2043,30 @@ remoteRepoSectionDescr =
20432043
localToRemote :: LocalRepo -> RemoteRepo
20442044
localToRemote (LocalRepo name path sharedCache) =
20452045
(emptyRemoteRepo name)
2046-
{ remoteRepoURI = URI "file+noindex:" Nothing path "" (if sharedCache then "#shared-cache" else "")
2046+
{ remoteRepoURI =
2047+
normaliseFileNoIndexURI buildOS $
2048+
URI
2049+
"file+noindex:"
2050+
(Just nullURIAuth)
2051+
path
2052+
""
2053+
(if sharedCache then "#shared-cache" else "")
20472054
}
20482055

2056+
-- | When on Windows, we need to convert the path to be POSIX-style.
2057+
--
2058+
-- >>> normaliseFileNoIndexURI Windows (URI "file+noindex:" (Just nullURIAuth) "\\\\.\\C:\\dev\\foo" "" "")
2059+
-- file+noindex:////./C:/dev/foo
2060+
--
2061+
-- See haddocks of 'asPosixPath' for some examples of why this is needed for
2062+
-- @network-uri@.
2063+
normaliseFileNoIndexURI :: OS -> URI -> URI
2064+
normaliseFileNoIndexURI os uri@(URI scheme auth path query fragment)
2065+
| "file+noindex:" <- scheme
2066+
, Windows <- os =
2067+
URI scheme auth (asPosixPath path) query fragment
2068+
| otherwise = uri
2069+
20492070
-------------------------------
20502071
-- Local field utils
20512072
--

cabal-install/tests/UnitTests/Distribution/Client/ProjectConfig.hs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import qualified Distribution.Simple.InstallDirs as InstallDirs
3333
import Distribution.Simple.Program.Db
3434
import Distribution.Simple.Program.Types
3535
import Distribution.Simple.Utils (toUTF8BS)
36+
import Distribution.System (OS (Windows), buildOS)
3637
import Distribution.Types.PackageVersionConstraint
3738
import Distribution.Version
3839

@@ -1016,7 +1017,10 @@ instance Arbitrary LocalRepo where
10161017
arbitrary =
10171018
LocalRepo
10181019
<$> arbitrary
1019-
<*> elements ["/tmp/foo", "/tmp/bar"] -- TODO: generate valid absolute paths
1020+
<*> elements
1021+
( (if buildOS == Windows then map (normalise . ("//./C:" ++)) else id)
1022+
["/tmp/foo", "/tmp/bar"]
1023+
) -- TODO: generate valid absolute paths
10201024
<*> arbitrary
10211025

10221026
instance Arbitrary PreSolver where

cabal-testsuite/src/Test/Cabal/OutputNormalizer.hs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ normalizeOutput nenv =
4646
. resub (posixRegexEscape "tmp/src-" ++ "[0-9]+") "<TMPDIR>"
4747
. resub (posixRegexEscape (normalizerTmpDir nenv) ++ sameDir) "<ROOT>/"
4848
. resub (posixRegexEscape (normalizerCanonicalTmpDir nenv) ++ sameDir) "<ROOT>/"
49-
-- Munge away C: prefix on filenames (Windows). We convert C:\\ to \\.
49+
-- Munge away C:\ prefix on filenames (Windows). We convert C:\ to \.
5050
. (if buildOS == Windows then resub "([A-Z]):\\\\" "\\\\" else id)
5151
. appEndo (F.fold (map (Endo . packageIdRegex) (normalizerKnownPackages nenv)))
5252
-- Look for 0.1/installed-0d6uzW7Ubh1Fb4TB5oeQ3G
@@ -78,6 +78,14 @@ normalizeOutput nenv =
7878
else id)
7979
. normalizeBuildInfoJson
8080
. maybe id normalizePathCmdOutput (normalizerCabalInstallVersion nenv)
81+
-- Munge away \\.\C:/ prefix on paths (Windows). We convert @\\.\C:/@ to
82+
-- @\@. We need to do this before the step above as that one would convert
83+
-- @\\.\@ to @\.\@.
84+
--
85+
-- These paths might come up in file+noindex URIs due to @filepath@
86+
-- normalizing @//./C:/foo.txt@ paths to @\\.\C:/foo.txt@, see
87+
-- (filepath#247).
88+
. (if buildOS == Windows then resub "\\\\\\\\\\.\\\\([A-Z]):/" "\\\\" else id)
8189
-- hackage-security locks occur non-deterministically
8290
. resub "(Released|Acquired|Waiting) .*hackage-security-lock\n" ""
8391
where

cabal-testsuite/src/Test/Cabal/Prelude.hs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ import Distribution.PackageDescription
4444
import Test.Utils.TempTestDir (withTestDir)
4545
import Distribution.Verbosity (normal)
4646
import Distribution.Utils.Path
47-
( makeSymbolicPath, relativeSymbolicPath, interpretSymbolicPathCWD )
47+
( asPosixPath, makeSymbolicPath, relativeSymbolicPath, interpretSymbolicPathCWD )
4848

4949
import Distribution.Compat.Stack
5050

@@ -614,9 +614,7 @@ withRepo repo_dir m = do
614614
-- TODO: Arguably should undo everything when we're done...
615615
where
616616
repoUri env ="file+noindex://" ++ (if isWindows
617-
then map (\x -> case x of
618-
'\\' -> '/'
619-
_ -> x)
617+
then joinDrive "//./" . asPosixPath
620618
else id) (testRepoDir env)
621619

622620
-- | Given a directory (relative to the 'testCurrentDir') containing

changelog.d/pr-10728

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
synopsis: Fix `file+noindex` URI usage on Windows
2+
packages: cabal-install
3+
prs: #10728
4+
issues: #10703
5+
significance:
6+
7+
description: {
8+
9+
- `file+noindex` repositories in Windows systems must use the format `file+noindex:////./C:/path/to/repo`.
10+
This syntax comes from https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats#dos-device-paths,
11+
and is the only syntax for DOS paths fully supported by the `network-uri` package, which Cabal uses to
12+
interpret URIs in repository stanzas.
13+
14+
}

doc/config.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,10 @@ repository.
200200
``package-name-version.tar.gz`` files in the directory, and will use optional
201201
corresponding ``package-name-version.cabal`` files as new revisions.
202202

203+
.. note::
204+
On Windows systems, the path has to be prefixed by ``//./`` as in
205+
``url: file+noindex:////./C:/absolute/path/to/directory``.
206+
203207
For example, if ``/absolute/path/to/directory`` looks like
204208
::
205209

0 commit comments

Comments
 (0)