2
2
{-# LANGUAGE DeriveGeneric #-}
3
3
{-# LANGUAGE EmptyDataDecls #-}
4
4
{-# LANGUAGE FlexibleContexts #-}
5
+ {-# LANGUAGE GADTs #-}
5
6
{-# LANGUAGE OverloadedStrings #-}
6
7
{-# LANGUAGE TemplateHaskell #-}
7
8
{-# LANGUAGE TupleSections #-}
@@ -20,6 +21,7 @@ module Stack.BuildPlan
20
21
, gpdPackageName
21
22
, MiniBuildPlan (.. )
22
23
, MiniPackageInfo (.. )
24
+ , loadResolver
23
25
, loadMiniBuildPlan
24
26
, removeSrcPkgDefaultFlags
25
27
, resolveBuildPlan
@@ -43,7 +45,7 @@ import Control.Monad.State.Strict (State, execState, get, modify,
43
45
import Control.Monad.Trans.Control (MonadBaseControl )
44
46
import qualified Crypto.Hash.SHA256 as SHA256
45
47
import Data.Aeson.Extended (WithJSONWarnings (.. ), logJSONWarnings )
46
- import Data.Binary.VersionTagged (taggedDecodeOrLoad )
48
+ import Data.Binary.VersionTagged (taggedDecodeOrLoad , decodeFileOrFailDeep )
47
49
import qualified Data.ByteString as S
48
50
import qualified Data.ByteString.Base16 as B16
49
51
import qualified Data.ByteString.Char8 as S8
@@ -55,13 +57,13 @@ import Data.List.NonEmpty (NonEmpty(..))
55
57
import qualified Data.List.NonEmpty as NonEmpty
56
58
import Data.Map (Map )
57
59
import qualified Data.Map as Map
58
- import Data.Maybe (fromMaybe , mapMaybe )
60
+ import Data.Maybe (fromMaybe , mapMaybe , isNothing )
59
61
import Data.Monoid
60
62
import Data.Set (Set )
61
63
import qualified Data.Set as Set
62
64
import Data.Text (Text )
63
65
import qualified Data.Text as T
64
- import Data.Text.Encoding (encodeUtf8 )
66
+ import Data.Text.Encoding (encodeUtf8 , decodeUtf8 )
65
67
import qualified Data.Traversable as Tr
66
68
import Data.Typeable (Typeable )
67
69
import Data.Yaml (decodeEither' , decodeFileEither )
@@ -86,8 +88,6 @@ import Stack.Package
86
88
import Stack.PackageIndex
87
89
import Stack.Types
88
90
import Stack.Types.StackT
89
- import qualified System.Directory as D
90
- import qualified System.FilePath as FP
91
91
92
92
data BuildPlanException
93
93
= UnknownPackages
@@ -96,6 +96,7 @@ data BuildPlanException
96
96
(Map PackageName (Set PackageIdentifier )) -- shadowed
97
97
| SnapshotNotFound SnapName
98
98
| FilepathInDownloadedSnapshot T. Text
99
+ | NeitherCompilerOrResolverSpecified T. Text
99
100
deriving (Typeable )
100
101
instance Exception BuildPlanException
101
102
instance Show BuildPlanException where
@@ -180,6 +181,10 @@ instance Show BuildPlanException where
180
181
, " field, but filepaths are not allowed in downloaded snapshots.\n "
181
182
, " Filepath specified: " ++ T. unpack url
182
183
]
184
+ show (NeitherCompilerOrResolverSpecified url) =
185
+ " Failed to load custom snapshot at " ++
186
+ T. unpack url ++
187
+ " , because no 'compiler' or 'resolver' is specified."
183
188
184
189
-- | Determine the necessary packages to install to have the given set of
185
190
-- packages available.
@@ -412,19 +417,25 @@ loadResolver
412
417
:: (MonadIO m , MonadThrow m , MonadLogger m , MonadReader env m , HasHttpManager env , HasConfig env , HasGHCVariant env , MonadBaseControl IO m , MonadCatch m )
413
418
=> Maybe (Path Abs File )
414
419
-> Resolver
415
- -> m MiniBuildPlan
420
+ -> m ( MiniBuildPlan , LoadedResolver )
416
421
loadResolver mconfigPath resolver =
417
422
case resolver of
418
- ResolverSnapshot snap -> loadMiniBuildPlan snap
423
+ ResolverSnapshot snap ->
424
+ liftM (, ResolverSnapshot snap) $ loadMiniBuildPlan snap
419
425
-- TODO(mgsloan): Not sure what this FIXME means
420
426
-- FIXME instead of passing the stackYaml dir we should maintain
421
427
-- the file URL in the custom resolver always relative to stackYaml.
422
- ResolverCustom _ url -> parseCustomMiniBuildPlan mconfigPath url
423
- ResolverCompiler compiler -> return MiniBuildPlan
424
- { mbpCompilerVersion = compiler
425
- , mbpPackages = mempty
426
- , mbpAllowNewer = False
427
- }
428
+ ResolverCustom name url -> do
429
+ (mbp, hash) <- parseCustomMiniBuildPlan mconfigPath url
430
+ return (mbp, ResolverCustomLoaded name url (decodeUtf8 hash))
431
+ ResolverCompiler compiler -> return
432
+ ( MiniBuildPlan
433
+ { mbpCompilerVersion = compiler
434
+ , mbpPackages = mempty
435
+ , mbpAllowNewer = False
436
+ }
437
+ , ResolverCompiler compiler
438
+ )
428
439
429
440
-- | Load up a 'MiniBuildPlan', preferably from cache
430
441
loadMiniBuildPlan
@@ -905,82 +916,147 @@ shadowMiniBuildPlan (MiniBuildPlan cv pkgs0 allowNewer) shadowed =
905
916
Just False -> Right
906
917
Nothing -> assert False Right
907
918
919
+ -- This works differently for snapshots fetched from URL and those
920
+ -- fetched from file:
921
+ --
922
+ -- 1) If downloading the snapshot from a URL, assume the fetched data is
923
+ -- immutable. Hash the URL in order to determine the location of the
924
+ -- cached download. The file contents of the snapshot determines the
925
+ -- hash for looking up cached MBP.
926
+ --
927
+ -- 2) If loading the snapshot from a file, load all of the involved
928
+ -- snapshot files. The hash used to determine the cached MBP is the hash
929
+ -- of the concatenation of the parent's hash with the snapshot contents.
930
+ --
931
+ -- Why this difference? We want to make it easy to simply edit snapshots
932
+ -- in the filesystem, but we want caching for remote snapshots. In order
933
+ -- to avoid reparsing / reloading all the yaml for remote snapshots, we
934
+ -- need a different hash system.
935
+
936
+ -- TODO: This could probably be more efficient if it first merged the
937
+ -- custom snapshots, and then applied them to the MBP. It is nice to
938
+ -- apply directly, because then we have the guarantee that it's
939
+ -- semantically identical to snapshot extension. If this optimization is
940
+ -- implemented, note that the direct Monoid for CustomSnapshot is not
941
+ -- correct. Crucially, if a package is present in the snapshot, its
942
+ -- flags and ghc-options are not based on settings from prior snapshots.
943
+ -- TODO: This semantics should be discussed / documented more.
944
+
945
+ -- TODO: allow a hash check in the resolver. This adds safety /
946
+ -- correctness, allowing you to ensure that you are indeed getting the
947
+ -- right custom snapshot.
948
+
949
+ -- TODO: Allow custom plan to specify a name.
950
+
908
951
parseCustomMiniBuildPlan
909
952
:: (MonadIO m , MonadCatch m , MonadLogger m , MonadReader env m , HasHttpManager env , HasConfig env , HasGHCVariant env , MonadBaseControl IO m )
910
953
=> Maybe (Path Abs File ) -- ^ Root directory for when url is a filepath
911
954
-> T. Text
912
- -> m MiniBuildPlan
913
- parseCustomMiniBuildPlan mconfigPath url0 = do
955
+ -> m ( MiniBuildPlan , S8. ByteString )
956
+ parseCustomMiniBuildPlan mconfigPath0 url0 = do
914
957
$ logDebug $ " Loading " <> url0 <> " build plan"
915
- eyamlFP <- getYamlFP url0
916
- let yamlFP = either id id eyamlFP
917
-
918
- -- FIXME: determine custom snapshot path based on contents. Ideally,
919
- -- use a hash scheme that ignores formatting differences (works on
920
- -- the data), so that an implicit snapshot (TBD) will hash to the
921
- -- same thing as a custom snapshot.
922
-
923
- yamlBS <- liftIO $ S. readFile $ toFilePath yamlFP
924
- let yamlHash = S8. unpack $ B16. encode $ SHA256. hash yamlBS
925
- binaryFilename <- parseRelFile $ yamlHash ++ " .bin"
926
- customPlanDir <- getCustomPlanDir
927
- let binaryFP = customPlanDir </> $ (mkRelDir " bin" ) </> binaryFilename
928
-
929
- taggedDecodeOrLoad binaryFP $ do
930
- WithJSONWarnings (cs0, mresolver) warnings <-
958
+ case parseUrl $ T. unpack url0 of
959
+ Just req -> downloadCustom url0 req
960
+ Nothing ->
961
+ case mconfigPath0 of
962
+ Nothing -> throwM $ FilepathInDownloadedSnapshot url0
963
+ Just configPath -> do
964
+ (getMbp, hash) <- readCustom configPath url0
965
+ mbp <- getMbp
966
+ return (mbp, hash)
967
+ where
968
+ downloadCustom url req = do
969
+ let urlHash = S8. unpack $ B16. encode $ SHA256. hash $ encodeUtf8 url
970
+ hashFP <- parseRelFile $ urlHash ++ " .yaml"
971
+ customPlanDir <- getCustomPlanDir
972
+ let cacheFP = customPlanDir </> $ (mkRelDir " yaml" ) </> hashFP
973
+ _ <- download req cacheFP
974
+ yamlBS <- liftIO $ S. readFile $ toFilePath cacheFP
975
+ let yamlHash = b16Hash yamlBS
976
+ binaryPath <- getBinaryPath yamlHash
977
+ liftM (, yamlHash) $ taggedDecodeOrLoad binaryPath $ do
978
+ (cs, mresolver) <- decodeYaml yamlBS
979
+ parentMbp <- case (csCompilerVersion cs, mresolver) of
980
+ (Nothing , Nothing ) -> throwM (NeitherCompilerOrResolverSpecified url)
981
+ (Just cv, Nothing ) -> return (compilerBuildPlan cv)
982
+ -- NOTE: ignoring the parent's hash, even though
983
+ -- there could be one. URL snapshot's hash are
984
+ -- determined just from their contents.
985
+ (_, Just resolver) -> liftM fst (loadResolver Nothing resolver)
986
+ applyCustomSnapshot cs parentMbp
987
+ readCustom configPath path = do
988
+ yamlFP <- resolveFile (parent configPath) (T. unpack $ fromMaybe path $
989
+ T. stripPrefix " file://" path <|> T. stripPrefix " file:" path)
990
+ yamlBS <- liftIO $ S. readFile $ toFilePath yamlFP
991
+ (cs, mresolver) <- decodeYaml yamlBS
992
+ (getMbp, hash) <- case mresolver of
993
+ Just (ResolverCustom _ url ) ->
994
+ case parseUrl $ T. unpack url of
995
+ Just req -> do
996
+ let getMbp = do
997
+ -- Ignore custom hash, under the
998
+ -- assumption that the URL is sufficient
999
+ -- for identity.
1000
+ (mbp, _) <- downloadCustom url req
1001
+ return mbp
1002
+ return (getMbp, b16Hash yamlBS)
1003
+ Nothing -> do
1004
+ (getMbp0, hash0) <- readCustom yamlFP url
1005
+ let hash = b16Hash (hash0 <> yamlBS)
1006
+ getMbp = do
1007
+ binaryPath <- getBinaryPath hash
1008
+ -- Idea here is to not waste time
1009
+ -- writing out intermediate cache files,
1010
+ -- but check for them.
1011
+ exists <- doesFileExist binaryPath
1012
+ if exists
1013
+ then do
1014
+ eres <- decodeFileOrFailDeep binaryPath
1015
+ case eres of
1016
+ Right (Just mbp) -> return mbp
1017
+ -- Invalid format cache file, remove.
1018
+ _ -> do
1019
+ removeFile binaryPath
1020
+ getMbp0
1021
+ else getMbp0
1022
+ return (getMbp, hash)
1023
+ Just resolver -> do
1024
+ -- NOTE: in the cases where we don't have a hash, the
1025
+ -- normal resolver name is enough. Since this name is
1026
+ -- part of the yaml file, it ends up in our hash.
1027
+ let hash = b16Hash yamlBS
1028
+ getMbp = do
1029
+ (mbp, resolver') <- loadResolver (Just configPath) resolver
1030
+ let mhash = customResolverHash resolver'
1031
+ assert (isNothing mhash) (return mbp)
1032
+ return (getMbp, hash)
1033
+ Nothing -> do
1034
+ case csCompilerVersion cs of
1035
+ Nothing -> throwM (NeitherCompilerOrResolverSpecified path)
1036
+ Just cv -> do
1037
+ let hash = b16Hash yamlBS
1038
+ getMbp = return (compilerBuildPlan cv)
1039
+ return (getMbp, hash)
1040
+ return (applyCustomSnapshot cs =<< getMbp, hash)
1041
+ getBinaryPath hash = do
1042
+ binaryFilename <- parseRelFile $ S8. unpack hash ++ " .bin"
1043
+ customPlanDir <- getCustomPlanDir
1044
+ return $ customPlanDir </> $ (mkRelDir " bin" ) </> binaryFilename
1045
+ decodeYaml yamlBS = do
1046
+ WithJSONWarnings res warnings <-
931
1047
either (throwM . ParseCustomSnapshotException url0) return $
932
1048
decodeEither' yamlBS
933
1049
logJSONWarnings (T. unpack url0) warnings
934
- case (mresolver, csCompilerVersion cs0) of
935
- (Nothing , Nothing ) ->
936
- fail $ " Failed to load custom snapshot at " ++
937
- T. unpack url0 ++
938
- " , because no 'compiler' or 'resolver' is specified."
939
- (Nothing , Just cv) ->
940
- applyCustomSnapshot cs0 MiniBuildPlan
941
- { mbpCompilerVersion = cv
942
- , mbpPackages = mempty
943
- , mbpAllowNewer = False
944
- }
945
- -- Even though we ignore the compiler version here, it gets
946
- -- used due to applyCustomSnapshot
947
- (Just resolver, _) -> do
948
- -- Load referenced resolver. If the custom snapshot is
949
- -- stored at a user location, then allow relative
950
- -- filepath custom snapshots.
951
- mbp <- loadResolver customFile resolver
952
- applyCustomSnapshot cs0 mbp
953
- where
954
- customFile = case eyamlFP of
955
- Left _ -> Nothing
956
- Right fp -> Just fp
957
- where
1050
+ return res
1051
+ compilerBuildPlan cv = MiniBuildPlan
1052
+ { mbpCompilerVersion = cv
1053
+ , mbpPackages = mempty
1054
+ , mbpAllowNewer = False
1055
+ }
958
1056
getCustomPlanDir = do
959
1057
root <- asks $ configStackRoot . getConfig
960
1058
return $ root </> $ (mkRelDir " custom-plan" )
961
-
962
- -- Get the path to the YAML file
963
- getYamlFP url =
964
- case parseUrl $ T. unpack url of
965
- Just req -> getYamlFPFromReq url req
966
- Nothing -> getYamlFPFromFile url
967
-
968
- getYamlFPFromReq url req = do
969
- let hashStr = S8. unpack $ B16. encode $ SHA256. hash $ encodeUtf8 url
970
- hashFP <- parseRelFile $ hashStr ++ " .yaml"
971
- customPlanDir <- getCustomPlanDir
972
-
973
- let cacheFP = customPlanDir </> $ (mkRelDir " yaml" ) </> hashFP
974
- _ <- download req cacheFP
975
- return (Left cacheFP)
976
-
977
- getYamlFPFromFile url =
978
- case mconfigPath of
979
- Nothing -> throwM $ FilepathInDownloadedSnapshot url
980
- Just configPath -> do
981
- fp <- liftIO $ D. canonicalizePath $ toFilePath (parent configPath) FP. </> T. unpack (fromMaybe url $
982
- T. stripPrefix " file://" url <|> T. stripPrefix " file:" url)
983
- Right <$> parseAbsFile fp
1059
+ b16Hash = B16. encode . SHA256. hash
984
1060
985
1061
applyCustomSnapshot
986
1062
:: (MonadIO m , MonadLogger m , MonadReader env m , HasHttpManager env , MonadThrow m , HasConfig env , MonadBaseControl IO m , MonadCatch m )
0 commit comments