Skip to content

Commit 529bd1f

Browse files
authored
Merge pull request #10728 from jasagredo/js/local-noindex
Fix file+noindex URI usage on Windows
2 parents 8e35c5d + e10db19 commit 529bd1f

File tree

11 files changed

+114
-36
lines changed

11 files changed

+114
-36
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: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ ghcid-cli: ## Run ghcid for the cabal-install executable.
125125

126126
.PHONY: doctest
127127
doctest: ## Run doctests.
128-
cd Cabal-syntax && $(DOCTEST)
128+
cd Cabal-syntax && $(DOCTEST) --build-depends=network-uri
129129
cd Cabal-described && $(DOCTEST)
130130
cd Cabal && $(DOCTEST)
131131
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
@@ -227,7 +227,8 @@ import System.Directory
227227
, renameFile
228228
)
229229
import System.FilePath
230-
( takeDirectory
230+
( normalise
231+
, takeDirectory
231232
, (<.>)
232233
, (</>)
233234
)
@@ -1693,7 +1694,14 @@ postProcessRepo lineno reponameStr repo0 = do
16931694
-- Note: the trailing colon is important
16941695
"file+noindex:" -> do
16951696
let uri = remoteRepoURI repo0
1696-
return $ Left $ LocalRepo reponame (uriPath uri) (uriFragment uri == "#shared-cache")
1697+
return $
1698+
Left $
1699+
LocalRepo
1700+
reponame
1701+
-- Normalization of Windows paths that use @//./@ does not fully
1702+
-- normalize the path (see filepath#247), but it is still usable.
1703+
(normalise (uriPath uri))
1704+
(uriFragment uri == "#shared-cache")
16971705
_ -> do
16981706
let repo = repo0{remoteRepoName = reponame}
16991707

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
@@ -466,7 +466,7 @@ readRepoIndex verbosity repoCtxt repo idxState =
466466
RepoSecure{..} -> warn verbosity $ exceptionMessageCabalInstall $ MissingPackageList repoRemote
467467
RepoLocalNoIndex local _ ->
468468
warn verbosity $
469-
"Error during construction of local+noindex "
469+
"Error during construction of file+noindex "
470470
++ unRepoName (localRepoName local)
471471
++ " repository index: "
472472
++ show e
@@ -526,7 +526,7 @@ whenCacheOutOfDate index action = do
526526
then action
527527
else
528528
if localNoIndex index
529-
then return () -- TODO: don't update cache for local+noindex repositories
529+
then return () -- TODO: don't update cache for file+noindex repositories
530530
else do
531531
indexTime <- getModTime $ indexFile index
532532
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
@@ -175,7 +175,7 @@ import Distribution.Simple.Command
175175
, option
176176
, reqArg'
177177
)
178-
import Distribution.System (Arch, OS)
178+
import Distribution.System (Arch, OS (Windows), buildOS)
179179
import Distribution.Types.PackageVersionConstraint
180180
( PackageVersionConstraint
181181
)
@@ -187,7 +187,7 @@ import Distribution.Utils.Path hiding
187187
import qualified Data.ByteString.Char8 as BS
188188
import qualified Data.Map as Map
189189
import qualified Data.Set as Set
190-
import Network.URI (URI (..), parseURI)
190+
import Network.URI (URI (..), nullURIAuth, parseURI)
191191
import System.Directory (createDirectoryIfMissing, makeAbsolute)
192192
import System.FilePath (isAbsolute, isPathSeparator, makeValid, splitFileName, (</>))
193193
import Text.PrettyPrint
@@ -2040,9 +2040,30 @@ remoteRepoSectionDescr =
20402040
localToRemote :: LocalRepo -> RemoteRepo
20412041
localToRemote (LocalRepo name path sharedCache) =
20422042
(emptyRemoteRepo name)
2043-
{ remoteRepoURI = URI "file+noindex:" Nothing path "" (if sharedCache then "#shared-cache" else "")
2043+
{ remoteRepoURI =
2044+
normaliseFileNoIndexURI buildOS $
2045+
URI
2046+
"file+noindex:"
2047+
(Just nullURIAuth)
2048+
path
2049+
""
2050+
(if sharedCache then "#shared-cache" else "")
20442051
}
20452052

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

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 & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -46,25 +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-
. (if buildOS == Windows
50-
then
51-
-- OK. Here's the deal. In `./Prelude.hs`, `withRepoNoUpdate` sets
52-
-- `repoUri` to the tmpdir but with backslashes replaced with
53-
-- slashes. This is because Windows treats backslashes and forward
54-
-- slashes largely the same in paths, and backslashes aren't allowed
55-
-- in a URL like `file+noindex://...`.
56-
--
57-
-- But that breaks the regexes above, which expect the paths to have
58-
-- backslashes.
59-
--
60-
-- Honestly this whole `normalizeOutput` thing is super janky and
61-
-- worth rewriting from the ground up. To you, poor soul in the
62-
-- future, here is one more hack upon a great pile. Hey, at least all
63-
-- the `PackageTests` function as a test suite for this thing...
64-
resub (posixRegexEscape (backslashToSlash $ normalizerTmpDir nenv) ++ sameDir) "<ROOT>/"
65-
. resub (posixRegexEscape (backslashToSlash $ normalizerCanonicalTmpDir nenv) ++ sameDir) "<ROOT>/"
66-
else id)
67-
-- Munge away C: prefix on filenames (Windows). We convert C:\\ to \\.
49+
-- Munge away C:\ prefix on filenames (Windows). We convert C:\ to \.
6850
. (if buildOS == Windows then resub "([A-Z]):\\\\" "\\\\" else id)
6951
. appEndo (F.fold (map (Endo . packageIdRegex) (normalizerKnownPackages nenv)))
7052
-- Look for 0.1/installed-0d6uzW7Ubh1Fb4TB5oeQ3G
@@ -96,6 +78,14 @@ normalizeOutput nenv =
9678
else id)
9779
. normalizeBuildInfoJson
9880
. 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)
9989
-- hackage-security locks occur non-deterministically
10090
. resub "(Released|Acquired|Waiting) .*hackage-security-lock\n" ""
10191
. resub "installed: [0-9]+(\\.[0-9]+)*" "installed: <VERSION>"

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ import Distribution.PackageDescription
4848
import Test.Utils.TempTestDir (withTestDir)
4949
import Distribution.Verbosity (normal)
5050
import Distribution.Utils.Path
51-
( makeSymbolicPath, relativeSymbolicPath, interpretSymbolicPathCWD )
51+
( asPosixPath, makeSymbolicPath, relativeSymbolicPath, interpretSymbolicPathCWD )
5252

5353
import Distribution.Compat.Stack
5454

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

621619
-- | 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)