Skip to content

Commit a07ef25

Browse files
committed
Implement file+noindex:///local/repositories
Resolve #6359 `preferred-versions` are left out for now. It shouldn't be difficult to add, but needs work nevertheless. We also allow relative paths, which kind of work, if you are careful. In addtition - change the index cache to use `Distribution.Utils.Structured`, making Binary instances generically derived. - separate Distribution.Client.HashValue into own module. This allows to use HashValue for hashing the part of localRepoPath (breaks module dependency cycle). Almost as a feature generated 01-index.cache is never updated. If you change the contents of the directory, you have to purge 01-index.cache file yourself.
1 parent 787b1f2 commit a07ef25

21 files changed

+601
-248
lines changed

Cabal/Distribution/Utils/Structured.hs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,10 @@ module Distribution.Utils.Structured (
4949
-- | These functions operate like @binary@'s counterparts,
5050
-- but the serialised version has a structure hash in front.
5151
structuredEncode,
52+
structuredEncodeFile,
5253
structuredDecode,
5354
structuredDecodeOrFailIO,
55+
structuredDecodeFileOrFail,
5456
-- * Structured class
5557
Structured (structure),
5658
MD5,
@@ -262,6 +264,10 @@ structuredEncode
262264
=> a -> LBS.ByteString
263265
structuredEncode x = Binary.encode (Tag :: Tag a, x)
264266

267+
-- | Lazily serialise a value to a file
268+
structuredEncodeFile :: (Binary.Binary a, Structured a) => FilePath -> a -> IO ()
269+
structuredEncodeFile f = LBS.writeFile f . structuredEncode
270+
265271
-- | Structured 'Binary.decode'.
266272
-- Decode a value from a lazy 'LBS.ByteString', reconstructing the original structure.
267273
-- Throws pure exception on invalid inputs.
@@ -280,6 +286,10 @@ structuredDecodeOrFailIO bs =
280286
handler (ErrorCall str) = return $ Left str
281287
#endif
282288

289+
-- | Lazily reconstruct a value previously written to a file.
290+
structuredDecodeFileOrFail :: (Binary.Binary a, Structured a) => FilePath -> IO (Either String a)
291+
structuredDecodeFileOrFail f = structuredDecodeOrFailIO =<< LBS.readFile f
292+
283293
-------------------------------------------------------------------------------
284294
-- Helper data
285295
-------------------------------------------------------------------------------

Cabal/doc/installing-packages.rst

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ The name of the repository is given on the first line, and can be
5757
anything; packages downloaded from this repository will be cached under
5858
``~/.cabal/packages/hackage.haskell.org`` (or whatever name you specify;
5959
you can change the prefix by changing the value of
60-
``remote-repo-cache``). If you want, you can configure multiple
60+
:cfg-field:`remote-repo-cache`). If you want, you can configure multiple
6161
repositories, and ``cabal`` will combine them and be able to download
6262
packages from any of them.
6363

@@ -97,7 +97,32 @@ received were the right ones. How that is done is however outside the
9797
scope of ``cabal`` proper.
9898

9999
More information about the security infrastructure can be found at
100-
https://github.com/well-typed/hackage-security.
100+
https://github.com/haskell/hackage-security.
101+
102+
Local no-index repositories
103+
^^^^^^^^^^^^^^^^^^^^^^^^^^^
104+
105+
It's possible to use a directory of `.tar.gz` package files as a local package
106+
repository.
107+
108+
::
109+
110+
repository my-local-repository
111+
url: file+noindex:///absolute/path/to/directory
112+
113+
``cabal`` will construct the index automatically from the
114+
``package-name-version.tar.gz`` files in the directory, and will use optional
115+
corresponding ``package-name-version.cabal`` files as new revisions.
116+
117+
The index is cached inside the given directory. If the directory is not
118+
writable, you can append ``#shared-cache`` fragment to the URI,
119+
then the cache will be stored inside the :cfg-field:`remote-repo-cache` directory.
120+
The part of the path will be used to determine the cache key part.
121+
122+
.. note::
123+
The URI scheme ``file:`` is interpreted as a remote repository,
124+
as described in the previous sections, thus requiring manual construction
125+
of ``01-index.tar`` file.
101126

102127
Legacy repositories
103128
^^^^^^^^^^^^^^^^^^^
@@ -120,7 +145,7 @@ although, in (and only in) the specific case of Hackage, the URL
120145
``http://hackage.haskell.org/packages/archive`` will be silently
121146
translated to ``http://hackage.haskell.org/``.
122147

123-
The second kind of legacy repositories are so-called “local”
148+
The second kind of legacy repositories are so-called “(legacy) local”
124149
repositories:
125150

126151
::

cabal-install/Distribution/Client/CmdUpdate.hs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,8 @@ updateRepo :: Verbosity -> UpdateFlags -> RepoContext -> (Repo, IndexState)
186186
updateRepo verbosity _updateFlags repoCtxt (repo, indexState) = do
187187
transport <- repoContextGetTransport repoCtxt
188188
case repo of
189-
RepoLocal{..} -> return ()
189+
RepoLocal{} -> return ()
190+
RepoLocalNoIndex{} -> return ()
190191
RepoRemote{..} -> do
191192
downloadResult <- downloadIndex transport verbosity
192193
repoRemote repoLocalDir

cabal-install/Distribution/Client/Config.hs

Lines changed: 65 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ module Distribution.Client.Config (
4141
userConfigUpdate,
4242
createDefaultConfigFile,
4343

44-
remoteRepoFields
44+
remoteRepoFields,
45+
postProcessRepo,
4546
) where
4647

4748
import Language.Haskell.Extension ( Language(Haskell2010) )
@@ -50,7 +51,7 @@ import Distribution.Deprecated.ViewAsFieldDescr
5051
( viewAsFieldDescr )
5152

5253
import Distribution.Client.Types
53-
( RemoteRepo(..), Username(..), Password(..), emptyRemoteRepo
54+
( RemoteRepo(..), LocalRepo (..), Username(..), Password(..), emptyRemoteRepo
5455
, AllowOlder(..), AllowNewer(..), RelaxDeps(..), isRelaxDeps
5556
)
5657
import Distribution.Client.BuildReports.Types
@@ -64,7 +65,7 @@ import Distribution.Client.Setup
6465
, InstallFlags(..), installOptions, defaultInstallFlags
6566
, UploadFlags(..), uploadCommand
6667
, ReportFlags(..), reportCommand
67-
, showRepo, parseRepo, readRepo )
68+
, showRemoteRepo, parseRemoteRepo, readRemoteRepo )
6869
import Distribution.Client.CmdInstall.ClientInstallFlags
6970
( ClientInstallFlags(..), defaultClientInstallFlags
7071
, clientInstallOptions )
@@ -92,7 +93,7 @@ import Distribution.Deprecated.ParseUtils
9293
, locatedErrorMsg, showPWarning
9394
, readFields, warning, lineNo
9495
, simpleField, listField, spaceListField
95-
, parseFilePathQ, parseOptCommaList, parseTokenQ )
96+
, parseFilePathQ, parseOptCommaList, parseTokenQ, syntaxError)
9697
import Distribution.Client.ParseUtils
9798
( parseFields, ppFields, ppSection )
9899
import Distribution.Client.HttpUtils
@@ -252,6 +253,7 @@ instance Semigroup SavedConfig where
252253
globalRemoteRepos = lastNonEmptyNL globalRemoteRepos,
253254
globalCacheDir = combine globalCacheDir,
254255
globalLocalRepos = lastNonEmptyNL globalLocalRepos,
256+
globalLocalNoIndexRepos = lastNonEmptyNL globalLocalNoIndexRepos,
255257
globalLogsDir = combine globalLogsDir,
256258
globalWorldFile = combine globalWorldFile,
257259
globalRequireSandbox = combine globalRequireSandbox,
@@ -1034,7 +1036,7 @@ deprecatedFieldDescriptions :: [FieldDescr SavedConfig]
10341036
deprecatedFieldDescriptions =
10351037
[ liftGlobalFlag $
10361038
listField "repos"
1037-
(Disp.text . showRepo) parseRepo
1039+
(Disp.text . showRemoteRepo) parseRemoteRepo
10381040
(fromNubList . globalRemoteRepos)
10391041
(\rs cfg -> cfg { globalRemoteRepos = toNubList rs })
10401042
, liftGlobalFlag $
@@ -1117,19 +1119,25 @@ parseConfig src initial = \str -> do
11171119
let init0 = savedInitFlags config
11181120
user0 = savedUserInstallDirs config
11191121
global0 = savedGlobalInstallDirs config
1120-
(remoteRepoSections0, haddockFlags, initFlags, user, global, paths, args) <-
1122+
(remoteRepoSections0, localRepoSections0, haddockFlags, initFlags, user, global, paths, args) <-
11211123
foldM parseSections
1122-
([], savedHaddockFlags config, init0, user0, global0, [], [])
1124+
([], [], savedHaddockFlags config, init0, user0, global0, [], [])
11231125
knownSections
11241126

11251127
let remoteRepoSections =
11261128
reverse
11271129
. nubBy ((==) `on` remoteRepoName)
11281130
$ remoteRepoSections0
11291131

1132+
let localRepoSections =
1133+
reverse
1134+
. nubBy ((==) `on` localRepoName)
1135+
$ localRepoSections0
1136+
11301137
return . fixConfigMultilines $ config {
11311138
savedGlobalFlags = (savedGlobalFlags config) {
11321139
globalRemoteRepos = toNubList remoteRepoSections,
1140+
globalLocalNoIndexRepos = toNubList localRepoSections,
11331141
-- the global extra prog path comes from the configure flag prog path
11341142
globalProgPathExtra = configProgramPathExtra (savedConfigureFlags config)
11351143
},
@@ -1185,68 +1193,92 @@ parseConfig src initial = \str -> do
11851193
parse = parseFields (configFieldDescriptions src
11861194
++ deprecatedFieldDescriptions) initial
11871195

1188-
parseSections (rs, h, i, u, g, p, a)
1189-
(ParseUtils.Section _ "repository" name fs) = do
1196+
parseSections (rs, ls, h, i, u, g, p, a)
1197+
(ParseUtils.Section lineno "repository" name fs) = do
11901198
r' <- parseFields remoteRepoFields (emptyRemoteRepo name) fs
1191-
when (remoteRepoKeyThreshold r' > length (remoteRepoRootKeys r')) $
1192-
warning $ "'key-threshold' for repository " ++ show (remoteRepoName r')
1193-
++ " higher than number of keys"
1194-
when (not (null (remoteRepoRootKeys r'))
1195-
&& remoteRepoSecure r' /= Just True) $
1196-
warning $ "'root-keys' for repository " ++ show (remoteRepoName r')
1197-
++ " non-empty, but 'secure' not set to True."
1198-
return (r':rs, h, i, u, g, p, a)
1199-
1200-
parseSections (rs, h, i, u, g, p, a)
1199+
r'' <- postProcessRepo lineno name r'
1200+
case r'' of
1201+
Left local -> return (rs, local:ls, h, i, u, g, p, a)
1202+
Right remote -> return (remote:rs, ls, h, i, u, g, p, a)
1203+
1204+
parseSections (rs, ls, h, i, u, g, p, a)
12011205
(ParseUtils.F lno "remote-repo" raw) = do
1202-
let mr' = readRepo raw
1206+
let mr' = readRemoteRepo raw
12031207
r' <- maybe (ParseFailed $ NoParse "remote-repo" lno) return mr'
1204-
return (r':rs, h, i, u, g, p, a)
1208+
return (r':rs, ls, h, i, u, g, p, a)
12051209

1206-
parseSections accum@(rs, h, i, u, g, p, a)
1210+
parseSections accum@(rs, ls, h, i, u, g, p, a)
12071211
(ParseUtils.Section _ "haddock" name fs)
12081212
| name == "" = do h' <- parseFields haddockFlagsFields h fs
1209-
return (rs, h', i, u, g, p, a)
1213+
return (rs, ls, h', i, u, g, p, a)
12101214
| otherwise = do
12111215
warning "The 'haddock' section should be unnamed"
12121216
return accum
12131217

1214-
parseSections accum@(rs, h, i, u, g, p, a)
1218+
parseSections accum@(rs, ls, h, i, u, g, p, a)
12151219
(ParseUtils.Section _ "init" name fs)
12161220
| name == "" = do i' <- parseFields initFlagsFields i fs
1217-
return (rs, h, i', u, g, p, a)
1221+
return (rs, ls, h, i', u, g, p, a)
12181222
| otherwise = do
12191223
warning "The 'init' section should be unnamed"
12201224
return accum
12211225

1222-
parseSections accum@(rs, h, i, u, g, p, a)
1226+
parseSections accum@(rs, ls, h, i, u, g, p, a)
12231227
(ParseUtils.Section _ "install-dirs" name fs)
12241228
| name' == "user" = do u' <- parseFields installDirsFields u fs
1225-
return (rs, h, i, u', g, p, a)
1229+
return (rs, ls, h, i, u', g, p, a)
12261230
| name' == "global" = do g' <- parseFields installDirsFields g fs
1227-
return (rs, h, i, u, g', p, a)
1231+
return (rs, ls, h, i, u, g', p, a)
12281232
| otherwise = do
12291233
warning "The 'install-paths' section should be for 'user' or 'global'"
12301234
return accum
12311235
where name' = lowercase name
1232-
parseSections accum@(rs, h, i, u, g, p, a)
1236+
parseSections accum@(rs, ls, h, i, u, g, p, a)
12331237
(ParseUtils.Section _ "program-locations" name fs)
12341238
| name == "" = do p' <- parseFields withProgramsFields p fs
1235-
return (rs, h, i, u, g, p', a)
1239+
return (rs, ls, h, i, u, g, p', a)
12361240
| otherwise = do
12371241
warning "The 'program-locations' section should be unnamed"
12381242
return accum
1239-
parseSections accum@(rs, h, i, u, g, p, a)
1243+
parseSections accum@(rs, ls, h, i, u, g, p, a)
12401244
(ParseUtils.Section _ "program-default-options" name fs)
12411245
| name == "" = do a' <- parseFields withProgramOptionsFields a fs
1242-
return (rs, h, i, u, g, p, a')
1246+
return (rs, ls, h, i, u, g, p, a')
12431247
| otherwise = do
12441248
warning "The 'program-default-options' section should be unnamed"
12451249
return accum
12461250
parseSections accum f = do
12471251
warning $ "Unrecognized stanza on line " ++ show (lineNo f)
12481252
return accum
12491253

1254+
postProcessRepo :: Int -> String -> RemoteRepo -> ParseResult (Either LocalRepo RemoteRepo)
1255+
postProcessRepo lineno reponame repo0 = do
1256+
when (null reponame) $
1257+
syntaxError lineno $ "a 'repository' section requires the "
1258+
++ "repository name as an argument"
1259+
1260+
case uriScheme (remoteRepoURI repo0) of
1261+
-- TODO: check that there are no authority, query or fragment
1262+
-- Note: the trailing colon is important
1263+
"file+noindex:" -> do
1264+
let uri = remoteRepoURI repo0
1265+
return $ Left $ LocalRepo reponame (uriPath uri) (uriFragment uri == "#shared-cache")
1266+
1267+
_ -> do
1268+
let repo = repo0 { remoteRepoName = reponame }
1269+
1270+
when (remoteRepoKeyThreshold repo > length (remoteRepoRootKeys repo)) $
1271+
warning $ "'key-threshold' for repository "
1272+
++ show (remoteRepoName repo)
1273+
++ " higher than number of keys"
1274+
1275+
when (not (null (remoteRepoRootKeys repo)) && remoteRepoSecure repo /= Just True) $
1276+
warning $ "'root-keys' for repository "
1277+
++ show (remoteRepoName repo)
1278+
++ " non-empty, but 'secure' not set to True."
1279+
1280+
return $ Right repo
1281+
12501282
showConfig :: SavedConfig -> String
12511283
showConfig = showConfigWithComments mempty
12521284

@@ -1297,7 +1329,7 @@ installDirsFields = map viewAsFieldDescr installDirsOptions
12971329

12981330
ppRemoteRepoSection :: RemoteRepo -> RemoteRepo -> Doc
12991331
ppRemoteRepoSection def vals = ppSection "repository" (remoteRepoName vals)
1300-
remoteRepoFields (Just def) vals
1332+
remoteRepoFields (Just def) vals
13011333

13021334
remoteRepoFields :: [FieldDescr RemoteRepo]
13031335
remoteRepoFields =

cabal-install/Distribution/Client/FetchUtils.hs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ fetchRepoTarball verbosity' repoCtxt repo pkgid = do
177177

178178
downloadRepoPackage = case repo of
179179
RepoLocal{..} -> return (packageFile repo pkgid)
180+
RepoLocalNoIndex{..} -> return (packageFile repo pkgid)
180181

181182
RepoRemote{..} -> do
182183
transport <- repoContextGetTransport repoCtxt
@@ -292,6 +293,7 @@ packageFile repo pkgid = packageDir repo pkgid
292293
-- the tarball for a given @PackageIdentifer@ is stored.
293294
--
294295
packageDir :: Repo -> PackageId -> FilePath
296+
packageDir (RepoLocalNoIndex (LocalRepo _ dir _) _) _pkgid = dir
295297
packageDir repo pkgid = repoLocalDir repo
296298
</> display (packageName pkgid)
297299
</> display (packageVersion pkgid)

0 commit comments

Comments
 (0)