1{-# LANGUAGE BangPatterns       #-}
2{-# LANGUAGE CPP                #-}
3{-# LANGUAGE DeriveDataTypeable #-}
4{-# LANGUAGE DeriveGeneric      #-}
5{-# LANGUAGE LambdaCase         #-}
6{-# LANGUAGE NamedFieldPuns     #-}
7{-# LANGUAGE RecordWildCards    #-}
8
9-- | Handling project configuration.
10--
11module Distribution.Client.ProjectConfig (
12
13    -- * Types for project config
14    ProjectConfig(..),
15    ProjectConfigBuildOnly(..),
16    ProjectConfigShared(..),
17    ProjectConfigProvenance(..),
18    PackageConfig(..),
19    MapLast(..),
20    MapMappend(..),
21
22    -- * Project root
23    findProjectRoot,
24    ProjectRoot(..),
25    BadProjectRoot(..),
26
27    -- * Project config files
28    readProjectConfig,
29    readGlobalConfig,
30    readProjectLocalFreezeConfig,
31    withProjectOrGlobalConfig,
32    withProjectOrGlobalConfigIgn,
33    writeProjectLocalExtraConfig,
34    writeProjectLocalFreezeConfig,
35    writeProjectConfigFile,
36    commandLineFlagsToProjectConfig,
37
38    -- * Packages within projects
39    ProjectPackageLocation(..),
40    BadPackageLocations(..),
41    BadPackageLocation(..),
42    BadPackageLocationMatch(..),
43    findProjectPackages,
44    fetchAndReadSourcePackages,
45
46    -- * Resolving configuration
47    lookupLocalPackageConfig,
48    projectConfigWithBuilderRepoContext,
49    projectConfigWithSolverRepoContext,
50    SolverSettings(..),
51    resolveSolverSettings,
52    BuildTimeSettings(..),
53    resolveBuildTimeSettings,
54
55    -- * Checking configuration
56    checkBadPerPackageCompilerPaths,
57    BadPerPackageCompilerPaths(..)
58  ) where
59
60import Prelude ()
61import Distribution.Client.Compat.Prelude
62
63import Distribution.Client.ProjectConfig.Types
64import Distribution.Client.ProjectConfig.Legacy
65import Distribution.Client.RebuildMonad
66import Distribution.Client.Glob
67         ( isTrivialFilePathGlob )
68import Distribution.Client.VCS
69         ( validateSourceRepos, SourceRepoProblem(..)
70         , VCS(..), knownVCSs, configureVCS, syncSourceRepos )
71
72import Distribution.Client.Types
73import Distribution.Client.DistDirLayout
74         ( DistDirLayout(..), CabalDirLayout(..), ProjectRoot(..) )
75import Distribution.Client.GlobalFlags
76         ( RepoContext(..), withRepoContext' )
77import Distribution.Client.BuildReports.Types
78         ( ReportLevel(..) )
79import Distribution.Client.Config
80         ( loadConfig, getConfigFilePath )
81import Distribution.Client.HttpUtils
82         ( HttpTransport, configureTransport, transportCheckHttps
83         , downloadURI )
84import Distribution.Client.Utils.Parsec (renderParseError)
85
86import Distribution.Solver.Types.SourcePackage
87import Distribution.Solver.Types.Settings
88import Distribution.Solver.Types.PackageConstraint
89         ( PackageProperty(..) )
90
91import Distribution.Package
92         ( PackageName, PackageId, packageId, UnitId )
93import Distribution.Types.PackageVersionConstraint
94         ( PackageVersionConstraint(..) )
95import Distribution.System
96         ( Platform )
97import Distribution.Types.GenericPackageDescription
98         ( GenericPackageDescription )
99import Distribution.PackageDescription.Parsec
100         ( parseGenericPackageDescription )
101import Distribution.Fields
102         ( runParseResult, PError, PWarning, showPWarning)
103import Distribution.Pretty (prettyShow)
104import Distribution.Types.SourceRepo
105         ( RepoType(..) )
106import Distribution.Client.SourceRepo
107         ( SourceRepoList, SourceRepositoryPackage (..), srpFanOut )
108import Distribution.Simple.Compiler
109         ( Compiler, compilerInfo )
110import Distribution.Simple.Program
111         ( ConfiguredProgram(..) )
112import Distribution.Simple.Setup
113         ( Flag(Flag), toFlag, flagToMaybe, flagToList
114         , fromFlag, fromFlagOrDefault )
115import Distribution.Client.Setup
116         ( defaultSolver, defaultMaxBackjumps )
117import Distribution.Simple.InstallDirs
118         ( PathTemplate, fromPathTemplate
119         , toPathTemplate, substPathTemplate, initialPathTemplateEnv )
120import Distribution.Simple.Utils
121         ( die', warn, notice, info, createDirectoryIfMissingVerbose )
122import Distribution.Client.Utils
123         ( determineNumJobs )
124import Distribution.Utils.NubList
125         ( fromNubList )
126import Distribution.Verbosity
127         ( Verbosity, modifyVerbosity, verbose )
128import Distribution.Version
129         ( Version )
130import Distribution.Deprecated.Text
131import qualified Distribution.Deprecated.ParseUtils as OldParser
132         ( ParseResult(..), locatedErrorMsg, showPWarning )
133
134import qualified Codec.Archive.Tar       as Tar
135import qualified Codec.Archive.Tar.Entry as Tar
136import qualified Distribution.Client.Tar as Tar
137import qualified Distribution.Client.GZipUtils as GZipUtils
138
139import Control.Monad
140import Control.Monad.Trans (liftIO)
141import Control.Exception
142import Data.Either
143import qualified Data.ByteString       as BS
144import qualified Data.ByteString.Lazy  as LBS
145import qualified Data.Map as Map
146import qualified Data.List.NonEmpty as NE
147import qualified Data.Set as Set
148import qualified Data.Hashable as Hashable
149import Numeric (showHex)
150
151import System.FilePath hiding (combine)
152import System.IO
153         ( withBinaryFile, IOMode(ReadMode) )
154import System.Directory
155import Network.URI
156         ( URI(..), URIAuth(..), parseAbsoluteURI, uriToString )
157
158
159----------------------------------------
160-- Resolving configuration to settings
161--
162
163-- | Look up a 'PackageConfig' field in the 'ProjectConfig' for a specific
164-- 'PackageName'. This returns the configuration that applies to all local
165-- packages plus any package-specific configuration for this package.
166--
167lookupLocalPackageConfig
168  :: (Semigroup a, Monoid a)
169  => (PackageConfig -> a) -> ProjectConfig -> PackageName
170  -> a
171lookupLocalPackageConfig field ProjectConfig {
172                           projectConfigLocalPackages,
173                           projectConfigSpecificPackage
174                         } pkgname =
175    field projectConfigLocalPackages
176 <> maybe mempty field
177          (Map.lookup pkgname (getMapMappend projectConfigSpecificPackage))
178
179
180-- | Use a 'RepoContext' based on the 'BuildTimeSettings'.
181--
182projectConfigWithBuilderRepoContext :: Verbosity
183                                    -> BuildTimeSettings
184                                    -> (RepoContext -> IO a) -> IO a
185projectConfigWithBuilderRepoContext verbosity BuildTimeSettings{..} =
186    withRepoContext'
187      verbosity
188      buildSettingRemoteRepos
189      buildSettingLocalRepos
190      buildSettingLocalNoIndexRepos
191      buildSettingCacheDir
192      buildSettingHttpTransport
193      (Just buildSettingIgnoreExpiry)
194      buildSettingProgPathExtra
195
196
197-- | Use a 'RepoContext', but only for the solver. The solver does not use the
198-- full facilities of the 'RepoContext' so we can get away with making one
199-- that doesn't have an http transport. And that avoids having to have access
200-- to the 'BuildTimeSettings'
201--
202projectConfigWithSolverRepoContext
203  :: Verbosity -> ProjectConfigShared -> ProjectConfigBuildOnly
204  -> (RepoContext -> IO a)
205  -> IO a
206projectConfigWithSolverRepoContext verbosity
207                                   ProjectConfigShared{..}
208                                   ProjectConfigBuildOnly{..} =
209    withRepoContext'
210      verbosity
211      (fromNubList projectConfigRemoteRepos)
212      (fromNubList projectConfigLocalRepos)
213      (fromNubList projectConfigLocalNoIndexRepos)
214      (fromFlagOrDefault
215                   (error
216                    "projectConfigWithSolverRepoContext: projectConfigCacheDir")
217                   projectConfigCacheDir)
218      (flagToMaybe projectConfigHttpTransport)
219      (flagToMaybe projectConfigIgnoreExpiry)
220      (fromNubList projectConfigProgPathExtra)
221
222
223-- | Resolve the project configuration, with all its optional fields, into
224-- 'SolverSettings' with no optional fields (by applying defaults).
225--
226resolveSolverSettings :: ProjectConfig -> SolverSettings
227resolveSolverSettings ProjectConfig{
228                        projectConfigShared,
229                        projectConfigLocalPackages,
230                        projectConfigSpecificPackage
231                      } =
232    SolverSettings {..}
233  where
234    --TODO: [required eventually] some of these settings need validation, e.g.
235    -- the flag assignments need checking.
236    solverSettingRemoteRepos       = fromNubList projectConfigRemoteRepos
237    solverSettingLocalRepos        = fromNubList projectConfigLocalRepos
238    solverSettingLocalNoIndexRepos = fromNubList projectConfigLocalNoIndexRepos
239    solverSettingConstraints       = projectConfigConstraints
240    solverSettingPreferences       = projectConfigPreferences
241    solverSettingFlagAssignment    = packageConfigFlagAssignment projectConfigLocalPackages
242    solverSettingFlagAssignments   = fmap packageConfigFlagAssignment
243                                          (getMapMappend projectConfigSpecificPackage)
244    solverSettingCabalVersion      = flagToMaybe projectConfigCabalVersion
245    solverSettingSolver            = fromFlag projectConfigSolver
246    solverSettingAllowOlder        = fromMaybe mempty projectConfigAllowOlder
247    solverSettingAllowNewer        = fromMaybe mempty projectConfigAllowNewer
248    solverSettingMaxBackjumps      = case fromFlag projectConfigMaxBackjumps of
249                                       n | n < 0     -> Nothing
250                                         | otherwise -> Just n
251    solverSettingReorderGoals      = fromFlag projectConfigReorderGoals
252    solverSettingCountConflicts    = fromFlag projectConfigCountConflicts
253    solverSettingFineGrainedConflicts = fromFlag projectConfigFineGrainedConflicts
254    solverSettingMinimizeConflictSet = fromFlag projectConfigMinimizeConflictSet
255    solverSettingStrongFlags       = fromFlag projectConfigStrongFlags
256    solverSettingAllowBootLibInstalls = fromFlag projectConfigAllowBootLibInstalls
257    solverSettingOnlyConstrained   = fromFlag projectConfigOnlyConstrained
258    solverSettingIndexState        = flagToMaybe projectConfigIndexState
259    solverSettingIndependentGoals  = fromFlag projectConfigIndependentGoals
260  --solverSettingShadowPkgs        = fromFlag projectConfigShadowPkgs
261  --solverSettingReinstall         = fromFlag projectConfigReinstall
262  --solverSettingAvoidReinstalls   = fromFlag projectConfigAvoidReinstalls
263  --solverSettingOverrideReinstall = fromFlag projectConfigOverrideReinstall
264  --solverSettingUpgradeDeps       = fromFlag projectConfigUpgradeDeps
265
266    ProjectConfigShared {..} = defaults <> projectConfigShared
267
268    defaults = mempty {
269       projectConfigSolver            = Flag defaultSolver,
270       projectConfigAllowOlder        = Just (AllowOlder mempty),
271       projectConfigAllowNewer        = Just (AllowNewer mempty),
272       projectConfigMaxBackjumps      = Flag defaultMaxBackjumps,
273       projectConfigReorderGoals      = Flag (ReorderGoals False),
274       projectConfigCountConflicts    = Flag (CountConflicts True),
275       projectConfigFineGrainedConflicts = Flag (FineGrainedConflicts True),
276       projectConfigMinimizeConflictSet = Flag (MinimizeConflictSet False),
277       projectConfigStrongFlags       = Flag (StrongFlags False),
278       projectConfigAllowBootLibInstalls = Flag (AllowBootLibInstalls False),
279       projectConfigOnlyConstrained   = Flag OnlyConstrainedNone,
280       projectConfigIndependentGoals  = Flag (IndependentGoals False)
281     --projectConfigShadowPkgs        = Flag False,
282     --projectConfigReinstall         = Flag False,
283     --projectConfigAvoidReinstalls   = Flag False,
284     --projectConfigOverrideReinstall = Flag False,
285     --projectConfigUpgradeDeps       = Flag False
286    }
287
288
289-- | Resolve the project configuration, with all its optional fields, into
290-- 'BuildTimeSettings' with no optional fields (by applying defaults).
291--
292resolveBuildTimeSettings :: Verbosity
293                         -> CabalDirLayout
294                         -> ProjectConfig
295                         -> BuildTimeSettings
296resolveBuildTimeSettings verbosity
297                         CabalDirLayout {
298                           cabalLogsDirectory
299                         }
300                         ProjectConfig {
301                           projectConfigShared = ProjectConfigShared {
302                             projectConfigRemoteRepos,
303                             projectConfigLocalRepos,
304                             projectConfigLocalNoIndexRepos,
305                             projectConfigProgPathExtra
306                           },
307                           projectConfigBuildOnly
308                         } =
309    BuildTimeSettings {..}
310  where
311    buildSettingDryRun        = fromFlag    projectConfigDryRun
312    buildSettingOnlyDeps      = fromFlag    projectConfigOnlyDeps
313    buildSettingSummaryFile   = fromNubList projectConfigSummaryFile
314    --buildSettingLogFile       -- defined below, more complicated
315    --buildSettingLogVerbosity  -- defined below, more complicated
316    buildSettingBuildReports  = fromFlag    projectConfigBuildReports
317    buildSettingSymlinkBinDir = flagToList  projectConfigSymlinkBinDir
318    buildSettingOneShot       = fromFlag    projectConfigOneShot
319    buildSettingNumJobs       = determineNumJobs projectConfigNumJobs
320    buildSettingKeepGoing     = fromFlag    projectConfigKeepGoing
321    buildSettingOfflineMode   = fromFlag    projectConfigOfflineMode
322    buildSettingKeepTempFiles = fromFlag    projectConfigKeepTempFiles
323    buildSettingRemoteRepos   = fromNubList projectConfigRemoteRepos
324    buildSettingLocalRepos    = fromNubList projectConfigLocalRepos
325    buildSettingLocalNoIndexRepos = fromNubList projectConfigLocalNoIndexRepos
326    buildSettingCacheDir      = fromFlag    projectConfigCacheDir
327    buildSettingHttpTransport = flagToMaybe projectConfigHttpTransport
328    buildSettingIgnoreExpiry  = fromFlag    projectConfigIgnoreExpiry
329    buildSettingReportPlanningFailure
330                              = fromFlag projectConfigReportPlanningFailure
331    buildSettingProgPathExtra = fromNubList projectConfigProgPathExtra
332
333    ProjectConfigBuildOnly{..} = defaults
334                              <> projectConfigBuildOnly
335
336    defaults = mempty {
337      projectConfigDryRun                = toFlag False,
338      projectConfigOnlyDeps              = toFlag False,
339      projectConfigBuildReports          = toFlag NoReports,
340      projectConfigReportPlanningFailure = toFlag False,
341      projectConfigKeepGoing             = toFlag False,
342      projectConfigOneShot               = toFlag False,
343      projectConfigOfflineMode           = toFlag False,
344      projectConfigKeepTempFiles         = toFlag False,
345      projectConfigIgnoreExpiry          = toFlag False
346    }
347
348    -- The logging logic: what log file to use and what verbosity.
349    --
350    -- If the user has specified --remote-build-reporting=detailed, use the
351    -- default log file location. If the --build-log option is set, use the
352    -- provided location. Otherwise don't use logging, unless building in
353    -- parallel (in which case the default location is used).
354    --
355    buildSettingLogFile :: Maybe (Compiler -> Platform
356                               -> PackageId -> UnitId -> FilePath)
357    buildSettingLogFile
358      | useDefaultTemplate = Just (substLogFileName defaultTemplate)
359      | otherwise          = fmap  substLogFileName givenTemplate
360
361    defaultTemplate = toPathTemplate $
362                        cabalLogsDirectory </>
363                        "$compiler" </> "$libname" <.> "log"
364    givenTemplate   = flagToMaybe projectConfigLogFile
365
366    useDefaultTemplate
367      | buildSettingBuildReports == DetailedReports = True
368      | isJust givenTemplate                        = False
369      | isParallelBuild                             = True
370      | otherwise                                   = False
371
372    isParallelBuild = buildSettingNumJobs >= 2
373
374    substLogFileName :: PathTemplate
375                     -> Compiler -> Platform
376                     -> PackageId -> UnitId -> FilePath
377    substLogFileName template compiler platform pkgid uid =
378        fromPathTemplate (substPathTemplate env template)
379      where
380        env = initialPathTemplateEnv
381                pkgid uid (compilerInfo compiler) platform
382
383    -- If the user has specified --remote-build-reporting=detailed or
384    -- --build-log, use more verbose logging.
385    --
386    buildSettingLogVerbosity
387      | overrideVerbosity = modifyVerbosity (max verbose) verbosity
388      | otherwise         = verbosity
389
390    overrideVerbosity
391      | buildSettingBuildReports == DetailedReports = True
392      | isJust givenTemplate                        = True
393      | isParallelBuild                             = False
394      | otherwise                                   = False
395
396
397---------------------------------------------
398-- Reading and writing project config files
399--
400
401-- | Find the root of this project.
402--
403-- Searches for an explicit @cabal.project@ file, in the current directory or
404-- parent directories. If no project file is found then the current dir is the
405-- project root (and the project will use an implicit config).
406--
407findProjectRoot :: Maybe FilePath -- ^ starting directory, or current directory
408                -> Maybe FilePath -- ^ @cabal.project@ file name override
409                -> IO (Either BadProjectRoot ProjectRoot)
410findProjectRoot _ (Just projectFile)
411  | isAbsolute projectFile = do
412    exists <- doesFileExist projectFile
413    if exists
414      then do projectFile' <- canonicalizePath projectFile
415              let projectRoot = ProjectRootExplicit (takeDirectory projectFile')
416                                                    (takeFileName projectFile')
417              return (Right projectRoot)
418      else return (Left (BadProjectRootExplicitFile projectFile))
419
420findProjectRoot mstartdir mprojectFile = do
421    startdir <- maybe getCurrentDirectory canonicalizePath mstartdir
422    homedir  <- getHomeDirectory
423    probe startdir homedir
424  where
425    projectFileName = fromMaybe "cabal.project" mprojectFile
426
427    -- Search upwards. If we get to the users home dir or the filesystem root,
428    -- then use the current dir
429    probe startdir homedir = go startdir
430      where
431        go dir | isDrive dir || dir == homedir =
432          case mprojectFile of
433            Nothing   -> return (Right (ProjectRootImplicit startdir))
434            Just file -> return (Left (BadProjectRootExplicitFile file))
435        go dir = do
436          exists <- doesFileExist (dir </> projectFileName)
437          if exists
438            then return (Right (ProjectRootExplicit dir projectFileName))
439            else go (takeDirectory dir)
440
441   --TODO: [nice to have] add compat support for old style sandboxes
442
443
444-- | Errors returned by 'findProjectRoot'.
445--
446data BadProjectRoot = BadProjectRootExplicitFile FilePath
447#if MIN_VERSION_base(4,8,0)
448  deriving (Show, Typeable)
449#else
450  deriving (Typeable)
451
452instance Show BadProjectRoot where
453  show = renderBadProjectRoot
454#endif
455
456instance Exception BadProjectRoot where
457#if MIN_VERSION_base(4,8,0)
458  displayException = renderBadProjectRoot
459#endif
460
461renderBadProjectRoot :: BadProjectRoot -> String
462renderBadProjectRoot (BadProjectRootExplicitFile projectFile) =
463    "The given project file '" ++ projectFile ++ "' does not exist."
464
465-- | Like 'withProjectOrGlobalConfig', with an additional boolean
466-- which tells to ignore local project.
467--
468-- Used to implement -z / --ignore-project behaviour
469--
470withProjectOrGlobalConfigIgn
471    :: Bool -- ^ whether to ignore local project
472    -> Verbosity
473    -> Flag FilePath
474    -> IO a
475    -> (ProjectConfig -> IO a)
476    -> IO a
477withProjectOrGlobalConfigIgn True  verbosity gcf _with without = do
478    globalConfig <- runRebuild "" $ readGlobalConfig verbosity gcf
479    without globalConfig
480withProjectOrGlobalConfigIgn False verbosity gcf with without =
481    withProjectOrGlobalConfig verbosity gcf with without
482
483withProjectOrGlobalConfig :: Verbosity
484                          -> Flag FilePath
485                          -> IO a
486                          -> (ProjectConfig -> IO a)
487                          -> IO a
488withProjectOrGlobalConfig verbosity globalConfigFlag with without = do
489  globalConfig <- runRebuild "" $ readGlobalConfig verbosity globalConfigFlag
490
491  let
492    res' = catch with
493      $ \case
494        (BadPackageLocations prov locs)
495          | prov == Set.singleton Implicit
496          , let
497            isGlobErr (BadLocGlobEmptyMatch _) = True
498            isGlobErr _ = False
499          , any isGlobErr locs ->
500            without globalConfig
501        err -> throwIO err
502
503  catch res'
504    $ \case
505      (BadProjectRootExplicitFile "") -> without globalConfig
506      err -> throwIO err
507
508-- | Read all the config relevant for a project. This includes the project
509-- file if any, plus other global config.
510--
511readProjectConfig :: Verbosity
512                  -> Flag FilePath
513                  -> DistDirLayout
514                  -> Rebuild ProjectConfig
515readProjectConfig verbosity configFileFlag distDirLayout = do
516    global <- readGlobalConfig                verbosity configFileFlag
517    local  <- readProjectLocalConfigOrDefault verbosity distDirLayout
518    freeze <- readProjectLocalFreezeConfig    verbosity distDirLayout
519    extra  <- readProjectLocalExtraConfig     verbosity distDirLayout
520    return (global <> local <> freeze <> extra)
521
522
523-- | Reads an explicit @cabal.project@ file in the given project root dir,
524-- or returns the default project config for an implicitly defined project.
525--
526readProjectLocalConfigOrDefault :: Verbosity
527                                -> DistDirLayout
528                                -> Rebuild ProjectConfig
529readProjectLocalConfigOrDefault verbosity distDirLayout = do
530  usesExplicitProjectRoot <- liftIO $ doesFileExist projectFile
531  if usesExplicitProjectRoot
532    then do
533      readProjectFile verbosity distDirLayout "" "project file"
534    else do
535      monitorFiles [monitorNonExistentFile projectFile]
536      return defaultImplicitProjectConfig
537
538  where
539    projectFile = distProjectFile distDirLayout ""
540
541    defaultImplicitProjectConfig :: ProjectConfig
542    defaultImplicitProjectConfig =
543      mempty {
544        -- We expect a package in the current directory.
545        projectPackages         = [ "./*.cabal" ],
546
547        -- This is to automatically pick up deps that we unpack locally.
548        projectPackagesOptional = [ "./*/*.cabal" ],
549
550        projectConfigProvenance = Set.singleton Implicit
551      }
552
553-- | Reads a @cabal.project.local@ file in the given project root dir,
554-- or returns empty. This file gets written by @cabal configure@, or in
555-- principle can be edited manually or by other tools.
556--
557readProjectLocalExtraConfig :: Verbosity -> DistDirLayout
558                            -> Rebuild ProjectConfig
559readProjectLocalExtraConfig verbosity distDirLayout =
560    readProjectFile verbosity distDirLayout "local"
561                             "project local configuration file"
562
563-- | Reads a @cabal.project.freeze@ file in the given project root dir,
564-- or returns empty. This file gets written by @cabal freeze@, or in
565-- principle can be edited manually or by other tools.
566--
567readProjectLocalFreezeConfig :: Verbosity -> DistDirLayout
568                             -> Rebuild ProjectConfig
569readProjectLocalFreezeConfig verbosity distDirLayout =
570    readProjectFile verbosity distDirLayout "freeze"
571                             "project freeze file"
572
573-- | Reads a named config file in the given project root dir, or returns empty.
574--
575readProjectFile :: Verbosity
576                -> DistDirLayout
577                -> String
578                -> String
579                -> Rebuild ProjectConfig
580readProjectFile verbosity DistDirLayout{distProjectFile}
581                         extensionName extensionDescription = do
582    exists <- liftIO $ doesFileExist extensionFile
583    if exists
584      then do monitorFiles [monitorFileHashed extensionFile]
585              addProjectFileProvenance <$> liftIO readExtensionFile
586      else do monitorFiles [monitorNonExistentFile extensionFile]
587              return mempty
588  where
589    extensionFile = distProjectFile extensionName
590
591    readExtensionFile =
592          reportParseResult verbosity extensionDescription extensionFile
593        . parseProjectConfig
594      =<< readFile extensionFile
595
596    addProjectFileProvenance config =
597      config {
598        projectConfigProvenance =
599          Set.insert (Explicit extensionFile) (projectConfigProvenance config)
600      }
601
602
603-- | Parse the 'ProjectConfig' format.
604--
605-- For the moment this is implemented in terms of parsers for legacy
606-- configuration types, plus a conversion.
607--
608parseProjectConfig :: String -> OldParser.ParseResult ProjectConfig
609parseProjectConfig content =
610    convertLegacyProjectConfig <$>
611      parseLegacyProjectConfig content
612
613
614-- | Render the 'ProjectConfig' format.
615--
616-- For the moment this is implemented in terms of a pretty printer for the
617-- legacy configuration types, plus a conversion.
618--
619showProjectConfig :: ProjectConfig -> String
620showProjectConfig =
621    showLegacyProjectConfig . convertToLegacyProjectConfig
622
623
624-- | Write a @cabal.project.local@ file in the given project root dir.
625--
626writeProjectLocalExtraConfig :: DistDirLayout -> ProjectConfig -> IO ()
627writeProjectLocalExtraConfig DistDirLayout{distProjectFile} =
628    writeProjectConfigFile (distProjectFile "local")
629
630
631-- | Write a @cabal.project.freeze@ file in the given project root dir.
632--
633writeProjectLocalFreezeConfig :: DistDirLayout -> ProjectConfig -> IO ()
634writeProjectLocalFreezeConfig DistDirLayout{distProjectFile} =
635    writeProjectConfigFile (distProjectFile "freeze")
636
637
638-- | Write in the @cabal.project@ format to the given file.
639--
640writeProjectConfigFile :: FilePath -> ProjectConfig -> IO ()
641writeProjectConfigFile file =
642    writeFile file . showProjectConfig
643
644
645-- | Read the user's @~/.cabal/config@ file.
646--
647readGlobalConfig :: Verbosity -> Flag FilePath -> Rebuild ProjectConfig
648readGlobalConfig verbosity configFileFlag = do
649    config     <- liftIO (loadConfig verbosity configFileFlag)
650    configFile <- liftIO (getConfigFilePath configFileFlag)
651    monitorFiles [monitorFileHashed configFile]
652    return (convertLegacyGlobalConfig config)
653
654reportParseResult :: Verbosity -> String -> FilePath -> OldParser.ParseResult a -> IO a
655reportParseResult verbosity _filetype filename (OldParser.ParseOk warnings x) = do
656    unless (null warnings) $
657      let msg = unlines (map (OldParser.showPWarning filename) warnings)
658       in warn verbosity msg
659    return x
660reportParseResult verbosity filetype filename (OldParser.ParseFailed err) =
661    let (line, msg) = OldParser.locatedErrorMsg err
662     in die' verbosity $ "Error parsing " ++ filetype ++ " " ++ filename
663           ++ maybe "" (\n -> ':' : show n) line ++ ":\n" ++ msg
664
665
666---------------------------------------------
667-- Finding packages in the project
668--
669
670-- | The location of a package as part of a project. Local file paths are
671-- either absolute (if the user specified it as such) or they are relative
672-- to the project root.
673--
674data ProjectPackageLocation =
675     ProjectPackageLocalCabalFile FilePath
676   | ProjectPackageLocalDirectory FilePath FilePath -- dir and .cabal file
677   | ProjectPackageLocalTarball   FilePath
678   | ProjectPackageRemoteTarball  URI
679   | ProjectPackageRemoteRepo     SourceRepoList
680   | ProjectPackageNamed          PackageVersionConstraint
681  deriving Show
682
683
684-- | Exception thrown by 'findProjectPackages'.
685--
686data BadPackageLocations
687   = BadPackageLocations (Set ProjectConfigProvenance) [BadPackageLocation]
688#if MIN_VERSION_base(4,8,0)
689  deriving (Show, Typeable)
690#else
691  deriving (Typeable)
692
693instance Show BadPackageLocations where
694  show = renderBadPackageLocations
695#endif
696
697instance Exception BadPackageLocations where
698#if MIN_VERSION_base(4,8,0)
699  displayException = renderBadPackageLocations
700#endif
701--TODO: [nice to have] custom exception subclass for Doc rendering, colour etc
702
703data BadPackageLocation
704   = BadPackageLocationFile    BadPackageLocationMatch
705   | BadLocGlobEmptyMatch      String
706   | BadLocGlobBadMatches      String [BadPackageLocationMatch]
707   | BadLocUnexpectedUriScheme String
708   | BadLocUnrecognisedUri     String
709   | BadLocUnrecognised        String
710  deriving Show
711
712data BadPackageLocationMatch
713   = BadLocUnexpectedFile      String
714   | BadLocNonexistantFile     String
715   | BadLocDirNoCabalFile      String
716   | BadLocDirManyCabalFiles   String
717  deriving Show
718
719renderBadPackageLocations :: BadPackageLocations -> String
720renderBadPackageLocations (BadPackageLocations provenance bpls)
721      -- There is no provenance information,
722      -- render standard bad package error information.
723    | Set.null provenance = renderErrors renderBadPackageLocation
724
725      -- The configuration is implicit, render bad package locations
726      -- using possibly specialized error messages.
727    | Set.singleton Implicit == provenance =
728        renderErrors renderImplicitBadPackageLocation
729
730      -- The configuration contains both implicit and explicit provenance.
731      -- This should not occur, and a message is output to assist debugging.
732    | Implicit `Set.member` provenance =
733           "Warning: both implicit and explicit configuration is present."
734        ++ renderExplicit
735
736      -- The configuration was read from one or more explicit path(s),
737      -- list the locations and render the bad package error information.
738      -- The intent is to supersede this with the relevant location information
739      -- per package error.
740    | otherwise = renderExplicit
741  where
742    renderErrors f = unlines (map f bpls)
743
744    renderExplicit =
745           "When using configuration(s) from "
746        ++ intercalate ", " (mapMaybe getExplicit (Set.toList provenance))
747        ++ ", the following errors occurred:\n"
748        ++ renderErrors renderBadPackageLocation
749
750    getExplicit (Explicit path) = Just path
751    getExplicit Implicit        = Nothing
752
753--TODO: [nice to have] keep track of the config file (and src loc) packages
754-- were listed, to use in error messages
755
756-- | Render bad package location error information for the implicit
757-- @cabal.project@ configuration.
758--
759-- TODO: This is currently not fully realized, with only one of the implicit
760-- cases handled. More cases should be added with informative help text
761-- about the issues related specifically when having no project configuration
762-- is present.
763renderImplicitBadPackageLocation :: BadPackageLocation -> String
764renderImplicitBadPackageLocation bpl = case bpl of
765    BadLocGlobEmptyMatch pkglocstr ->
766        "No cabal.project file or cabal file matching the default glob '"
767     ++ pkglocstr ++ "' was found.\n"
768     ++ "Please create a package description file <pkgname>.cabal "
769     ++ "or a cabal.project file referencing the packages you "
770     ++ "want to build."
771    _ -> renderBadPackageLocation bpl
772
773renderBadPackageLocation :: BadPackageLocation -> String
774renderBadPackageLocation bpl = case bpl of
775    BadPackageLocationFile badmatch ->
776        renderBadPackageLocationMatch badmatch
777    BadLocGlobEmptyMatch pkglocstr ->
778        "The package location glob '" ++ pkglocstr
779     ++ "' does not match any files or directories."
780    BadLocGlobBadMatches pkglocstr failures ->
781        "The package location glob '" ++ pkglocstr ++ "' does not match any "
782     ++ "recognised forms of package. "
783     ++ concatMap ((' ':) . renderBadPackageLocationMatch) failures
784    BadLocUnexpectedUriScheme pkglocstr ->
785        "The package location URI '" ++ pkglocstr ++ "' does not use a "
786     ++ "supported URI scheme. The supported URI schemes are http, https and "
787     ++ "file."
788    BadLocUnrecognisedUri pkglocstr ->
789        "The package location URI '" ++ pkglocstr ++ "' does not appear to "
790     ++ "be a valid absolute URI."
791    BadLocUnrecognised pkglocstr ->
792        "The package location syntax '" ++ pkglocstr ++ "' is not recognised."
793
794renderBadPackageLocationMatch :: BadPackageLocationMatch -> String
795renderBadPackageLocationMatch bplm = case bplm of
796    BadLocUnexpectedFile pkglocstr ->
797        "The package location '" ++ pkglocstr ++ "' is not recognised. The "
798     ++ "supported file targets are .cabal files, .tar.gz tarballs or package "
799     ++ "directories (i.e. directories containing a .cabal file)."
800    BadLocNonexistantFile pkglocstr ->
801        "The package location '" ++ pkglocstr ++ "' does not exist."
802    BadLocDirNoCabalFile pkglocstr ->
803        "The package directory '" ++ pkglocstr ++ "' does not contain any "
804     ++ ".cabal file."
805    BadLocDirManyCabalFiles pkglocstr ->
806        "The package directory '" ++ pkglocstr ++ "' contains multiple "
807     ++ ".cabal files (which is not currently supported)."
808
809-- | Given the project config,
810--
811-- Throws 'BadPackageLocations'.
812--
813findProjectPackages :: DistDirLayout -> ProjectConfig
814                    -> Rebuild [ProjectPackageLocation]
815findProjectPackages DistDirLayout{distProjectRootDirectory}
816                    ProjectConfig{..} = do
817
818    requiredPkgs <- findPackageLocations True    projectPackages
819    optionalPkgs <- findPackageLocations False   projectPackagesOptional
820    let repoPkgs  = map ProjectPackageRemoteRepo projectPackagesRepo
821        namedPkgs = map ProjectPackageNamed      projectPackagesNamed
822
823    return (concat [requiredPkgs, optionalPkgs, repoPkgs, namedPkgs])
824  where
825    findPackageLocations required pkglocstr = do
826      (problems, pkglocs) <-
827        partitionEithers <$> mapM (findPackageLocation required) pkglocstr
828      unless (null problems) $
829        liftIO $ throwIO $ BadPackageLocations projectConfigProvenance problems
830      return (concat pkglocs)
831
832
833    findPackageLocation :: Bool -> String
834                        -> Rebuild (Either BadPackageLocation
835                                          [ProjectPackageLocation])
836    findPackageLocation _required@True pkglocstr =
837      -- strategy: try first as a file:// or http(s):// URL.
838      -- then as a file glob (usually encompassing single file)
839      -- finally as a single file, for files that fail to parse as globs
840                    checkIsUriPackage pkglocstr
841      `mplusMaybeT` checkIsFileGlobPackage pkglocstr
842      `mplusMaybeT` checkIsSingleFilePackage pkglocstr
843      >>= maybe (return (Left (BadLocUnrecognised pkglocstr))) return
844
845
846    findPackageLocation _required@False pkglocstr = do
847      -- just globs for optional case
848      res <- checkIsFileGlobPackage pkglocstr
849      case res of
850        Nothing              -> return (Left (BadLocUnrecognised pkglocstr))
851        Just (Left _)        -> return (Right []) -- it's optional
852        Just (Right pkglocs) -> return (Right pkglocs)
853
854
855    checkIsUriPackage, checkIsFileGlobPackage, checkIsSingleFilePackage
856      :: String -> Rebuild (Maybe (Either BadPackageLocation
857                                         [ProjectPackageLocation]))
858    checkIsUriPackage pkglocstr =
859      case parseAbsoluteURI pkglocstr of
860        Just uri@URI {
861            uriScheme    = scheme,
862            uriAuthority = Just URIAuth { uriRegName = host },
863            uriPath      = path,
864            uriQuery     = query,
865            uriFragment  = frag
866          }
867          | recognisedScheme && not (null host) ->
868            return (Just (Right [ProjectPackageRemoteTarball uri]))
869
870          | scheme == "file:" && null host && null query && null frag ->
871            checkIsSingleFilePackage path
872
873          | not recognisedScheme && not (null host) ->
874            return (Just (Left (BadLocUnexpectedUriScheme pkglocstr)))
875
876          | recognisedScheme && null host ->
877            return (Just (Left (BadLocUnrecognisedUri pkglocstr)))
878          where
879            recognisedScheme = scheme == "http:" || scheme == "https:"
880                            || scheme == "file:"
881
882        _ -> return Nothing
883
884
885    checkIsFileGlobPackage pkglocstr =
886      case simpleParse pkglocstr of
887        Nothing   -> return Nothing
888        Just glob -> liftM Just $ do
889          matches <- matchFileGlob glob
890          case matches of
891            [] | isJust (isTrivialFilePathGlob glob)
892               -> return (Left (BadPackageLocationFile
893                                  (BadLocNonexistantFile pkglocstr)))
894
895            [] -> return (Left (BadLocGlobEmptyMatch pkglocstr))
896
897            _  -> do
898              (failures, pkglocs) <- partitionEithers <$>
899                                     mapM checkFilePackageMatch matches
900              return $! case (failures, pkglocs) of
901                ([failure], []) | isJust (isTrivialFilePathGlob glob)
902                        -> Left (BadPackageLocationFile failure)
903                (_, []) -> Left (BadLocGlobBadMatches pkglocstr failures)
904                _       -> Right pkglocs
905
906
907    checkIsSingleFilePackage pkglocstr = do
908      let filename = distProjectRootDirectory </> pkglocstr
909      isFile <- liftIO $ doesFileExist filename
910      isDir  <- liftIO $ doesDirectoryExist filename
911      if isFile || isDir
912        then checkFilePackageMatch pkglocstr
913         >>= either (return . Just . Left  . BadPackageLocationFile)
914                    (return . Just . Right . (\x->[x]))
915        else return Nothing
916
917
918    checkFilePackageMatch :: String -> Rebuild (Either BadPackageLocationMatch
919                                                       ProjectPackageLocation)
920    checkFilePackageMatch pkglocstr = do
921      -- The pkglocstr may be absolute or may be relative to the project root.
922      -- Either way, </> does the right thing here. We return relative paths if
923      -- they were relative in the first place.
924      let abspath = distProjectRootDirectory </> pkglocstr
925      isFile <- liftIO $ doesFileExist abspath
926      isDir  <- liftIO $ doesDirectoryExist abspath
927      parentDirExists <- case takeDirectory abspath of
928                           []  -> return False
929                           dir -> liftIO $ doesDirectoryExist dir
930      case () of
931        _ | isDir
932         -> do matches <- matchFileGlob (globStarDotCabal pkglocstr)
933               case matches of
934                 [cabalFile]
935                     -> return (Right (ProjectPackageLocalDirectory
936                                         pkglocstr cabalFile))
937                 []  -> return (Left (BadLocDirNoCabalFile pkglocstr))
938                 _   -> return (Left (BadLocDirManyCabalFiles pkglocstr))
939
940          | extensionIsTarGz pkglocstr
941         -> return (Right (ProjectPackageLocalTarball pkglocstr))
942
943          | takeExtension pkglocstr == ".cabal"
944         -> return (Right (ProjectPackageLocalCabalFile pkglocstr))
945
946          | isFile
947         -> return (Left (BadLocUnexpectedFile pkglocstr))
948
949          | parentDirExists
950         -> return (Left (BadLocNonexistantFile pkglocstr))
951
952          | otherwise
953         -> return (Left (BadLocUnexpectedFile pkglocstr))
954
955
956    extensionIsTarGz f = takeExtension f                 == ".gz"
957                      && takeExtension (dropExtension f) == ".tar"
958
959
960-- | A glob to find all the cabal files in a directory.
961--
962-- For a directory @some/dir/@, this is a glob of the form @some/dir/\*.cabal@.
963-- The directory part can be either absolute or relative.
964--
965globStarDotCabal :: FilePath -> FilePathGlob
966globStarDotCabal dir =
967    FilePathGlob
968      (if isAbsolute dir then FilePathRoot root else FilePathRelative)
969      (foldr (\d -> GlobDir [Literal d])
970             (GlobFile [WildCard, Literal ".cabal"]) dirComponents)
971  where
972    (root, dirComponents) = fmap splitDirectories (splitDrive dir)
973
974
975--TODO: [code cleanup] use sufficiently recent transformers package
976mplusMaybeT :: Monad m => m (Maybe a) -> m (Maybe a) -> m (Maybe a)
977mplusMaybeT ma mb = do
978  mx <- ma
979  case mx of
980    Nothing -> mb
981    Just x  -> return (Just x)
982
983
984-------------------------------------------------
985-- Fetching and reading packages in the project
986--
987
988-- | Read the @.cabal@ files for a set of packages. For remote tarballs and
989-- VCS source repos this also fetches them if needed.
990--
991-- Note here is where we convert from project-root relative paths to absolute
992-- paths.
993--
994fetchAndReadSourcePackages
995  :: Verbosity
996  -> DistDirLayout
997  -> ProjectConfigShared
998  -> ProjectConfigBuildOnly
999  -> [ProjectPackageLocation]
1000  -> Rebuild [PackageSpecifier (SourcePackage UnresolvedPkgLoc)]
1001fetchAndReadSourcePackages verbosity distDirLayout
1002                           projectConfigShared
1003                           projectConfigBuildOnly
1004                           pkgLocations = do
1005
1006    pkgsLocalDirectory <-
1007      sequence
1008        [ readSourcePackageLocalDirectory verbosity dir cabalFile
1009        | location <- pkgLocations
1010        , (dir, cabalFile) <- projectPackageLocal location ]
1011
1012    pkgsLocalTarball <-
1013      sequence
1014        [ readSourcePackageLocalTarball verbosity path
1015        | ProjectPackageLocalTarball path <- pkgLocations ]
1016
1017    pkgsRemoteTarball <- do
1018      getTransport <- delayInitSharedResource $
1019                      configureTransport verbosity progPathExtra
1020                                         preferredHttpTransport
1021      sequence
1022        [ fetchAndReadSourcePackageRemoteTarball verbosity distDirLayout
1023                                                 getTransport uri
1024        | ProjectPackageRemoteTarball uri <- pkgLocations ]
1025
1026    pkgsRemoteRepo <-
1027      syncAndReadSourcePackagesRemoteRepos
1028        verbosity distDirLayout
1029        projectConfigShared
1030        [ repo | ProjectPackageRemoteRepo repo <- pkgLocations ]
1031
1032    let pkgsNamed =
1033          [ NamedPackage pkgname [PackagePropertyVersion verrange]
1034          | ProjectPackageNamed (PackageVersionConstraint pkgname verrange) <- pkgLocations ]
1035
1036    return $ concat
1037      [ pkgsLocalDirectory
1038      , pkgsLocalTarball
1039      , pkgsRemoteTarball
1040      , pkgsRemoteRepo
1041      , pkgsNamed
1042      ]
1043  where
1044    projectPackageLocal (ProjectPackageLocalDirectory dir file) = [(dir, file)]
1045    projectPackageLocal (ProjectPackageLocalCabalFile     file) = [(dir, file)]
1046                                                where dir = takeDirectory file
1047    projectPackageLocal _ = []
1048
1049    progPathExtra = fromNubList (projectConfigProgPathExtra projectConfigShared)
1050    preferredHttpTransport =
1051      flagToMaybe (projectConfigHttpTransport projectConfigBuildOnly)
1052
1053-- | A helper for 'fetchAndReadSourcePackages' to handle the case of
1054-- 'ProjectPackageLocalDirectory' and 'ProjectPackageLocalCabalFile'.
1055-- We simply read the @.cabal@ file.
1056--
1057readSourcePackageLocalDirectory
1058  :: Verbosity
1059  -> FilePath  -- ^ The package directory
1060  -> FilePath  -- ^ The package @.cabal@ file
1061  -> Rebuild (PackageSpecifier (SourcePackage UnresolvedPkgLoc))
1062readSourcePackageLocalDirectory verbosity dir cabalFile = do
1063    monitorFiles [monitorFileHashed cabalFile]
1064    root <- askRoot
1065    let location = LocalUnpackedPackage (root </> dir)
1066    liftIO $ fmap (mkSpecificSourcePackage location)
1067           . readSourcePackageCabalFile verbosity cabalFile
1068         =<< BS.readFile (root </> cabalFile)
1069
1070
1071-- | A helper for 'fetchAndReadSourcePackages' to handle the case of
1072-- 'ProjectPackageLocalTarball'. We scan through the @.tar.gz@ file to find
1073-- the @.cabal@ file and read that.
1074--
1075readSourcePackageLocalTarball
1076  :: Verbosity
1077  -> FilePath
1078  -> Rebuild (PackageSpecifier (SourcePackage UnresolvedPkgLoc))
1079readSourcePackageLocalTarball verbosity tarballFile = do
1080    monitorFiles [monitorFile tarballFile]
1081    root <- askRoot
1082    let location = LocalTarballPackage (root </> tarballFile)
1083    liftIO $ fmap (mkSpecificSourcePackage location)
1084           . uncurry (readSourcePackageCabalFile verbosity)
1085         =<< extractTarballPackageCabalFile (root </> tarballFile)
1086
1087-- | A helper for 'fetchAndReadSourcePackages' to handle the case of
1088-- 'ProjectPackageRemoteTarball'. We download the tarball to the dist src dir
1089-- and after that handle it like the local tarball case.
1090--
1091fetchAndReadSourcePackageRemoteTarball
1092  :: Verbosity
1093  -> DistDirLayout
1094  -> Rebuild HttpTransport
1095  -> URI
1096  -> Rebuild (PackageSpecifier (SourcePackage UnresolvedPkgLoc))
1097fetchAndReadSourcePackageRemoteTarball verbosity
1098                                       DistDirLayout {
1099                                         distDownloadSrcDirectory
1100                                       }
1101                                       getTransport
1102                                       tarballUri =
1103    -- The tarball download is expensive so we use another layer of file
1104    -- monitor to avoid it whenever possible.
1105    rerunIfChanged verbosity monitor tarballUri $ do
1106
1107      -- Download
1108      transport <- getTransport
1109      liftIO $ do
1110        transportCheckHttps verbosity transport tarballUri
1111        notice verbosity ("Downloading " ++ show tarballUri)
1112        createDirectoryIfMissingVerbose verbosity True
1113                                        distDownloadSrcDirectory
1114        _ <- downloadURI transport verbosity tarballUri tarballFile
1115        return ()
1116
1117      -- Read
1118      monitorFiles [monitorFile tarballFile]
1119      let location = RemoteTarballPackage tarballUri tarballFile
1120      liftIO $ fmap (mkSpecificSourcePackage location)
1121             . uncurry (readSourcePackageCabalFile verbosity)
1122           =<< extractTarballPackageCabalFile tarballFile
1123  where
1124    tarballStem = distDownloadSrcDirectory
1125              </> localFileNameForRemoteTarball tarballUri
1126    tarballFile = tarballStem <.> "tar.gz"
1127
1128    monitor :: FileMonitor URI (PackageSpecifier (SourcePackage UnresolvedPkgLoc))
1129    monitor = newFileMonitor (tarballStem <.> "cache")
1130
1131
1132-- | A helper for 'fetchAndReadSourcePackages' to handle all the cases of
1133-- 'ProjectPackageRemoteRepo'.
1134--
1135syncAndReadSourcePackagesRemoteRepos
1136  :: Verbosity
1137  -> DistDirLayout
1138  -> ProjectConfigShared
1139  -> [SourceRepoList]
1140  -> Rebuild [PackageSpecifier (SourcePackage UnresolvedPkgLoc)]
1141syncAndReadSourcePackagesRemoteRepos verbosity
1142                                     DistDirLayout{distDownloadSrcDirectory}
1143                                     ProjectConfigShared {
1144                                       projectConfigProgPathExtra
1145                                     }
1146                                    repos = do
1147
1148    repos' <- either reportSourceRepoProblems return $
1149              validateSourceRepos repos
1150
1151    -- All 'SourceRepo's grouped by referring to the "same" remote repo
1152    -- instance. So same location but can differ in commit/tag/branch/subdir.
1153    let reposByLocation :: Map (RepoType, String)
1154                               [(SourceRepoList, RepoType)]
1155        reposByLocation = Map.fromListWith (++)
1156                            [ ((rtype, rloc), [(repo, vcsRepoType vcs)])
1157                            | (repo, rloc, rtype, vcs) <- repos' ]
1158
1159    --TODO: pass progPathExtra on to 'configureVCS'
1160    let _progPathExtra = fromNubList projectConfigProgPathExtra
1161    getConfiguredVCS <- delayInitSharedResources $ \repoType ->
1162                          let vcs = Map.findWithDefault (error $ "Unknown VCS: " ++ prettyShow repoType) repoType knownVCSs in
1163                          configureVCS verbosity {-progPathExtra-} vcs
1164
1165    concat <$> sequence
1166      [ rerunIfChanged verbosity monitor repoGroup' $ do
1167          vcs' <- getConfiguredVCS repoType
1168          syncRepoGroupAndReadSourcePackages vcs' pathStem repoGroup'
1169      | repoGroup@((primaryRepo, repoType):_) <- Map.elems reposByLocation
1170      , let repoGroup' = map fst repoGroup
1171            pathStem = distDownloadSrcDirectory
1172                   </> localFileNameForRemoteRepo primaryRepo
1173            monitor :: FileMonitor
1174                         [SourceRepoList]
1175                         [PackageSpecifier (SourcePackage UnresolvedPkgLoc)]
1176            monitor  = newFileMonitor (pathStem <.> "cache")
1177      ]
1178  where
1179    syncRepoGroupAndReadSourcePackages
1180      :: VCS ConfiguredProgram
1181      -> FilePath
1182      -> [SourceRepoList]
1183      -> Rebuild [PackageSpecifier (SourcePackage UnresolvedPkgLoc)]
1184    syncRepoGroupAndReadSourcePackages vcs pathStem repoGroup = do
1185        liftIO $ createDirectoryIfMissingVerbose verbosity False
1186                                                 distDownloadSrcDirectory
1187
1188        -- For syncing we don't care about different 'SourceRepo' values that
1189        -- are just different subdirs in the same repo.
1190        syncSourceRepos verbosity vcs
1191          [ (repo, repoPath)
1192          | (repo, _, repoPath) <- repoGroupWithPaths ]
1193
1194        -- But for reading we go through each 'SourceRepo' including its subdir
1195        -- value and have to know which path each one ended up in.
1196        sequence
1197          [ readPackageFromSourceRepo repoWithSubdir repoPath
1198          | (_, reposWithSubdir, repoPath) <- repoGroupWithPaths
1199          , repoWithSubdir <- NE.toList reposWithSubdir ]
1200      where
1201        -- So to do both things above, we pair them up here.
1202        repoGroupWithPaths
1203          :: [(SourceRepositoryPackage Proxy, NonEmpty (SourceRepositoryPackage Maybe), FilePath)]
1204        repoGroupWithPaths =
1205          zipWith (\(x, y) z -> (x,y,z))
1206                  (mapGroup
1207                      [ (repo { srpSubdir = Proxy }, repo)
1208                      | repo <- foldMap (NE.toList . srpFanOut) repoGroup
1209                      ])
1210                  repoPaths
1211
1212        mapGroup :: Ord k => [(k, v)] -> [(k, NonEmpty v)]
1213        mapGroup = Map.toList . Map.fromListWith (<>) . map (\(k, v) -> (k, pure v))
1214
1215        -- The repos in a group are given distinct names by simple enumeration
1216        -- foo, foo-2, foo-3 etc
1217        repoPaths :: [FilePath]
1218        repoPaths = pathStem
1219                  : [ pathStem ++ "-" ++ show (i :: Int) | i <- [2..] ]
1220
1221    readPackageFromSourceRepo
1222        :: SourceRepositoryPackage Maybe -> FilePath
1223        -> Rebuild (PackageSpecifier (SourcePackage UnresolvedPkgLoc))
1224    readPackageFromSourceRepo repo repoPath = do
1225        let packageDir = maybe repoPath (repoPath </>) (srpSubdir repo)
1226        entries <- liftIO $ getDirectoryContents packageDir
1227        --TODO: wrap exceptions
1228        case filter (\e -> takeExtension e == ".cabal") entries of
1229          []       -> liftIO $ throwIO $ NoCabalFileFound packageDir
1230          (_:_:_)  -> liftIO $ throwIO $ MultipleCabalFilesFound packageDir
1231          [cabalFileName] -> do
1232            monitorFiles [monitorFileHashed cabalFilePath]
1233            liftIO $ fmap (mkSpecificSourcePackage location)
1234                   . readSourcePackageCabalFile verbosity cabalFilePath
1235                 =<< BS.readFile cabalFilePath
1236            where
1237              cabalFilePath = packageDir </> cabalFileName
1238              location      = RemoteSourceRepoPackage repo packageDir
1239
1240
1241    reportSourceRepoProblems :: [(SourceRepoList, SourceRepoProblem)] -> Rebuild a
1242    reportSourceRepoProblems = liftIO . die' verbosity . renderSourceRepoProblems
1243
1244    renderSourceRepoProblems :: [(SourceRepoList, SourceRepoProblem)] -> String
1245    renderSourceRepoProblems = unlines . map show -- "TODO: the repo problems"
1246
1247
1248-- | Utility used by all the helpers of 'fetchAndReadSourcePackages' to make an
1249-- appropriate @'PackageSpecifier' ('SourcePackage' (..))@ for a given package
1250-- from a given location.
1251--
1252mkSpecificSourcePackage :: PackageLocation FilePath
1253                        -> GenericPackageDescription
1254                        -> PackageSpecifier
1255                             (SourcePackage (PackageLocation (Maybe FilePath)))
1256mkSpecificSourcePackage location pkg =
1257    SpecificSourcePackage SourcePackage {
1258      packageInfoId        = packageId pkg,
1259      packageDescription   = pkg,
1260      --TODO: it is silly that we still have to use a Maybe FilePath here
1261      packageSource        = fmap Just location,
1262      packageDescrOverride = Nothing
1263    }
1264
1265
1266-- | Errors reported upon failing to parse a @.cabal@ file.
1267--
1268data CabalFileParseError = CabalFileParseError
1269    FilePath           -- ^ @.cabal@ file path
1270    BS.ByteString      -- ^ @.cabal@ file contents
1271    (NonEmpty PError)  -- ^ errors
1272    (Maybe Version)    -- ^ We might discover the spec version the package needs
1273    [PWarning]         -- ^ warnings
1274  deriving (Typeable)
1275
1276-- | Manual instance which skips file contentes
1277instance Show CabalFileParseError where
1278    showsPrec d (CabalFileParseError fp _ es mv ws) = showParen (d > 10)
1279        $ showString "CabalFileParseError"
1280        . showChar ' ' . showsPrec 11 fp
1281        . showChar ' ' . showsPrec 11 ("" :: String)
1282        . showChar ' ' . showsPrec 11 es
1283        . showChar ' ' . showsPrec 11 mv
1284        . showChar ' ' . showsPrec 11 ws
1285
1286instance Exception CabalFileParseError
1287#if MIN_VERSION_base(4,8,0)
1288  where
1289  displayException = renderCabalFileParseError
1290#endif
1291
1292renderCabalFileParseError :: CabalFileParseError -> String
1293renderCabalFileParseError (CabalFileParseError filePath contents errors _ warnings) =
1294    renderParseError filePath contents errors warnings
1295
1296-- | Wrapper for the @.cabal@ file parser. It reports warnings on higher
1297-- verbosity levels and throws 'CabalFileParseError' on failure.
1298--
1299readSourcePackageCabalFile :: Verbosity
1300                           -> FilePath
1301                           -> BS.ByteString
1302                           -> IO GenericPackageDescription
1303readSourcePackageCabalFile verbosity pkgfilename content =
1304    case runParseResult (parseGenericPackageDescription content) of
1305      (warnings, Right pkg) -> do
1306        unless (null warnings) $
1307          info verbosity (formatWarnings warnings)
1308        return pkg
1309
1310      (warnings, Left (mspecVersion, errors)) ->
1311        throwIO $ CabalFileParseError pkgfilename content errors mspecVersion warnings
1312  where
1313    formatWarnings warnings =
1314        "The package description file " ++ pkgfilename
1315     ++ " has warnings: "
1316     ++ unlines (map (showPWarning pkgfilename) warnings)
1317
1318
1319-- | When looking for a package's @.cabal@ file we can find none, or several,
1320-- both of which are failures.
1321--
1322data CabalFileSearchFailure
1323   = NoCabalFileFound FilePath
1324   | MultipleCabalFilesFound FilePath
1325  deriving (Show, Typeable)
1326
1327instance Exception CabalFileSearchFailure
1328
1329
1330-- | Find the @.cabal@ file within a tarball file and return it by value.
1331--
1332-- Can fail with a 'Tar.FormatError' or 'CabalFileSearchFailure' exception.
1333--
1334extractTarballPackageCabalFile :: FilePath -> IO (FilePath, BS.ByteString)
1335extractTarballPackageCabalFile tarballFile =
1336    withBinaryFile tarballFile ReadMode $ \hnd -> do
1337      content <- LBS.hGetContents hnd
1338      case extractTarballPackageCabalFilePure tarballFile content of
1339        Left (Left  e) -> throwIO e
1340        Left (Right e) -> throwIO e
1341        Right (fileName, fileContent) ->
1342          (,) fileName <$> evaluate (LBS.toStrict fileContent)
1343
1344
1345-- | Scan through a tar file stream and collect the @.cabal@ file, or fail.
1346--
1347extractTarballPackageCabalFilePure :: FilePath
1348                                   -> LBS.ByteString
1349                                   -> Either (Either Tar.FormatError
1350                                                     CabalFileSearchFailure)
1351                                             (FilePath, LBS.ByteString)
1352extractTarballPackageCabalFilePure tarballFile =
1353      check
1354    . accumEntryMap
1355    . Tar.filterEntries isCabalFile
1356    . Tar.read
1357    . GZipUtils.maybeDecompress
1358  where
1359    accumEntryMap = Tar.foldlEntries
1360                      (\m e -> Map.insert (Tar.entryTarPath e) e m)
1361                      Map.empty
1362
1363    check (Left (e, _m)) = Left (Left e)
1364    check (Right m) = case Map.elems m of
1365        []     -> Left (Right $ NoCabalFileFound tarballFile)
1366        [file] -> case Tar.entryContent file of
1367          Tar.NormalFile content _ -> Right (Tar.entryPath file, content)
1368          _                        -> Left (Right $ NoCabalFileFound tarballFile)
1369        _files -> Left (Right $ MultipleCabalFilesFound tarballFile)
1370
1371    isCabalFile e = case splitPath (Tar.entryPath e) of
1372      [     _dir, file] -> takeExtension file == ".cabal"
1373      [".", _dir, file] -> takeExtension file == ".cabal"
1374      _                 -> False
1375
1376
1377-- | The name to use for a local file for a remote tarball 'SourceRepo'.
1378-- This is deterministic based on the remote tarball URI, and is intended
1379-- to produce non-clashing file names for different tarballs.
1380--
1381localFileNameForRemoteTarball :: URI -> FilePath
1382localFileNameForRemoteTarball uri =
1383    mangleName uri
1384 ++ "-" ++  showHex locationHash ""
1385  where
1386    mangleName = truncateString 10 . dropExtension . dropExtension
1387               . takeFileName . dropTrailingPathSeparator . uriPath
1388
1389    locationHash :: Word
1390    locationHash = fromIntegral (Hashable.hash (uriToString id uri ""))
1391
1392
1393-- | The name to use for a local file or dir for a remote 'SourceRepo'.
1394-- This is deterministic based on the source repo identity details, and
1395-- intended to produce non-clashing file names for different repos.
1396--
1397localFileNameForRemoteRepo :: SourceRepoList -> FilePath
1398localFileNameForRemoteRepo SourceRepositoryPackage {srpType, srpLocation} =
1399    mangleName srpLocation ++ "-" ++ showHex locationHash ""
1400  where
1401    mangleName = truncateString 10 . dropExtension
1402               . takeFileName . dropTrailingPathSeparator
1403
1404    -- just the parts that make up the "identity" of the repo
1405    locationHash :: Word
1406    locationHash =
1407      fromIntegral (Hashable.hash (show srpType, srpLocation))
1408
1409
1410-- | Truncate a string, with a visual indication that it is truncated.
1411truncateString :: Int -> String -> String
1412truncateString n s | length s <= n = s
1413                   | otherwise     = take (n-1) s ++ "_"
1414
1415
1416-- TODO: add something like this, here or in the project planning
1417-- Based on the package location, which packages will be built inplace in the
1418-- build tree vs placed in the store. This has various implications on what we
1419-- can do with the package, e.g. can we run tests, ghci etc.
1420--
1421-- packageIsLocalToProject :: ProjectPackageLocation -> Bool
1422
1423
1424---------------------------------------------
1425-- Checking configuration sanity
1426--
1427
1428data BadPerPackageCompilerPaths
1429   = BadPerPackageCompilerPaths [(PackageName, String)]
1430#if MIN_VERSION_base(4,8,0)
1431  deriving (Show, Typeable)
1432#else
1433  deriving (Typeable)
1434
1435instance Show BadPerPackageCompilerPaths where
1436  show = renderBadPerPackageCompilerPaths
1437#endif
1438
1439instance Exception BadPerPackageCompilerPaths where
1440#if MIN_VERSION_base(4,8,0)
1441  displayException = renderBadPerPackageCompilerPaths
1442#endif
1443--TODO: [nice to have] custom exception subclass for Doc rendering, colour etc
1444
1445renderBadPerPackageCompilerPaths :: BadPerPackageCompilerPaths -> String
1446renderBadPerPackageCompilerPaths
1447  (BadPerPackageCompilerPaths ((pkgname, progname) : _)) =
1448    "The path to the compiler program (or programs used by the compiler) "
1449 ++ "cannot be specified on a per-package basis in the cabal.project file "
1450 ++ "(i.e. setting the '" ++ progname ++ "-location' for package '"
1451 ++ display pkgname ++ "'). All packages have to use the same compiler, so "
1452 ++ "specify the path in a global 'program-locations' section."
1453 --TODO: [nice to have] better format control so we can pretty-print the
1454 -- offending part of the project file. Currently the line wrapping breaks any
1455 -- formatting.
1456renderBadPerPackageCompilerPaths _ = error "renderBadPerPackageCompilerPaths"
1457
1458-- | The project configuration is not allowed to specify program locations for
1459-- programs used by the compiler as these have to be the same for each set of
1460-- packages.
1461--
1462-- We cannot check this until we know which programs the compiler uses, which
1463-- in principle is not until we've configured the compiler.
1464--
1465-- Throws 'BadPerPackageCompilerPaths'
1466--
1467checkBadPerPackageCompilerPaths :: [ConfiguredProgram]
1468                                -> Map PackageName PackageConfig
1469                                -> IO ()
1470checkBadPerPackageCompilerPaths compilerPrograms packagesConfig =
1471    case [ (pkgname, progname)
1472         | let compProgNames = Set.fromList (map programId compilerPrograms)
1473         ,  (pkgname, pkgconf) <- Map.toList packagesConfig
1474         , progname <- Map.keys (getMapLast (packageConfigProgramPaths pkgconf))
1475         , progname `Set.member` compProgNames ] of
1476      [] -> return ()
1477      ps -> throwIO (BadPerPackageCompilerPaths ps)
1478